GEA 2017, Week 4 February 21, 2017 1. Problem 1 After debugging the program through GDB, we can see that an allocated memory buffer has been freed twice. At the time foo(...) gets called in the main function, a temporary object is created which will be destroyed after foo(...) returns. In the body of foo(...), another local object x is created, whose copy assignment operator gets later called with par as an argument. Since, we have not overloaded the copy assignment operator ourselves, the compiler has generated the default one which just copies all of the members using their assignment operators. In this case, the x member in both objects points to the same memory location. Just before foo(...) returns, x will be destroyed essentially calling free from the standard library and freeing the memory pointed to by x. Afterwards, par will be destroyed. Since its member x also points to the same memory location, free gets called on an already freed memory buffer. If we had implemented a move assignment operator or move constructor, then the copy assignment operator would not have been automatically generated. Actually, it would have been declared, but deleted and thus its usage forbidden! In our case, there really isn t any reason we should not follow the rule of 5 and overload additionally the move assigment operator, move constructor, copy constructor and copy assigment operator. X e x p l i c i t X( int s z = 10) : s z ( s z ), x (new int [ s z ] ) f o r ( int i = 0 ; i < sz ; ++i ) x [ i ] = 0 ; X( ) delete [ ] x ; X( const X& r h t ) : s z ( r h t. s z ), x (new int [ r h t. s z ] ) // copy d a t a s t d : : copy ( r h t. x, r h t. x + r h t. s z, x ) ; X(X&& r h t ) noexcept : s z ( r h t. s z ), 1
x ( r h t. x ) r h t. s z = 0 ; r h t. x = n u l l p t r ; X& operator=(x&& r h t ) noexcept i f ( t h i s == &r h t ) // d e s t r o y o b j e c t s a l l o c a t e d d a t a delete [ ] x ; // s t e a l d a t a from r h t s z = r h t. s z ; r h t. s z = 0 ; x = r h t. x ; r h t. x = n u l l p t r ; X& operator=(const X& r h t ) i f ( t h i s == &r h t ) // A l l o c a t e new b u f f e r. We f i r s t a l l o c a t e t h e memory b e f o r e c h a n g i n g t h e s t a t e i n any way. // T h i s p r o v i d e s t h e s t r o n g e x c e p t i o n g u a r a n t e e. int tmp = new int [ r h t. s z ] ; s z = r h t. s z ; // D e s t r o y o b j e c t s a l l o c a t e d d a t a. delete [ ] x ; x = tmp ; // copy d a t a s t d : : copy ( r h t. x, r h t. x + r h t. s z, x ) ; private : int s z, x ; ; void foo ( const X& par ) X x ; x = par ; f o o (X( ) ) ; 2. Problem 2 (a) inline specifier The inline specifiers instruct the compiler that the prefered calling mechanism is to insert the body of the function instead of the default calling mechanism. Whether the compiler will comply is implementation dependent. Compilers usually have stronger specifiers like forceinline under MSVC. A function cannot always be inlined for various reasons: https://msdn.microsoft.com/enus/library/bw1hbe6y.aspx On some systems there can be a strict limit on the size of the executable program which renders inlining unfavourable. (b) include guards A way to prevent the same defition to be added multiple times in a translation unit. 2
Example: File Hello.hpp: #i f n d e f HELLO HPP #define HELLO HPP H e l l o ; #endif // HELLO HPP File Something.hpp #i f n d e f SOMETHING HPP #define SOMETHING HPP #include Hello. hpp void p r i n t S t u f f ( const H e l l o &smt ) ; #endif // SOMETHING HPP File main.cpp #include #include Hello. hpp Something. hpp p r i n t S t u f f ( H e l l o ( ) ) ; If there weren t include guards in Hello.hpp, then there would have been a compilation error as the class Hello would have been defined twice. Mistakes can happen whenever include guards are used. For example, one might check for one macro being defined, but define another after the check. One might also check/define the same macro in multiple headers. #pragma once is not part of the standard, but it is widely supported. (c) pure virtual function A virtual function with added a pure-specifier. If you check the C++11 grammar, you can see that the pure-specifier is just = 0. If a class has a pure virtual function, it cannot be instantiated and thus becomes an abstract class. The class can be only used as a base class. If the derived class does not declare all pure virtual functions without the pure-specifier, then it is also an abstract class. You can consult the C++11 standard, 10.4 Abstract classes for more cases. Also, an implementation can be provided to a pure virtual function. ; X v i r t u a l void print ( ) = 0 ; void X : : p r i n t ( ) cout << X p r i n t s << e n d l ; Y : public X 3
; void p r i n t ( ) o v e r r i d e cout << Y p r i n t s << e n d l ; Z : public X void p r i n t ( ) o v e r r i d e X : : p r i n t ( ) ; ; X t1 = new Y( ) ; X t2 = new Z ( ) ; t1 >p r i n t ( ) ; // Y p r i n t s t2 >p r i n t ( ) ; // X p r i n t s (d) virtual destructor If a destructor is declared virtual in a base class, the derived class destructor will be implicitly declared virtual as well. This will guarantee that whenever delete gets called, the most-derived class destructor will be called all up to the base s class destructor (in reverse order of the constructors being called). ; X v i r t u a l X( ) cout << d e l X << endl ; Z : public X Z ( ) // i m p l i c i t l y v i r t u a l cout << d e l Z << endl ; ; D : public Z D( ) // i m p l i c i t l y v i r t u a l cout << d e l D << endl ; ; Z t = new D( ) ; delete t ; It prints: del D del Z del X (e) private inheritance A 4
.. a n y t h i n g e l s e needed.. ; int mpublicdata ; protected : int mprotecteddata ; private : int mprivatedata ; B : private A.. o t h e r s t u f f h e r e.. Because B inherits from A privately, any member method of B can access the public and protected fields of A. However, non of A s members can be accessed through an instance of B. For example: A a (new A( ) ) ; // j u s t f i n e b e c a u s e mpublicdata i s p u b l i c and can b e a c c e s s e d t h r o u g h an i n s t a n c e o f A. a >mpublicdata = 1 ; B b (new B ( ) ) ; // n o t f i n e, b e c a u s e a c c e s s i n g mpublicdata t h r o u g h // a B i n s t a n c e would b e t h e same a s i f we a r e a c c e s s i n g a p r i v a t e member. b >mpublicdata = 1 ; (f) scoped enumeration An alternative to plain enum in which the enumerators do not pollute the global scope. Example: enum ; Color red, black enum Animal : int dog, cat ; Color c1 = red ; // r e d i s a r e s e r v e d name int c2 = b l a c k ; // c a s t s t o i n t Animal a1 = Animal : : dog ; // h a s t o a c c e s s t h e c o r r e c t s c o p e f i r s t int a2 = s t a t i c c a s t <int >(Animal : : c a t ) ; // h a s t o b e e x p l i c i t l y c a s t e d t o i n t 3. Problem 3 There are many different situations in which one would rely on C pointers: (a) Libraries or the code base require it. (b) Complying with the coding standard. For example, check https://chromium.googlesource.com/a under Other C++ Features. (c) Whenever a reference cannot be initialized at the moment of declaration. 5
S l i d e r S l i d e r ( ) : mboundvalue ( n u l l p t r ) // e v e r y t h i n g e l s e r e q u i r e d void bindtoslider ( f l o a t var ) mboundvariable = var ; private : f l o a t mboundvariable ; The example above is a Slider which receives a slider by the user. Initially, there is no bound variable to the slider. Thus, we cannot have mboundvariable to be a reference. Very much similarly we cannot use reference types in containers which require copying. I.e. std :: vector < MyT ype& > will result in a compilation error. Note that one can use std :: reference wrapper < MyT ype >, but it really just wraps a MyType pointer. Smart pointers are a way of preventing memory leaks, but they are still not great. One still has to think of the ownership of resources. Possibly a rule of thumb is to initialize pointers immediately - either to nullptr or make them point to the appropriate place in memory. After freeing the memory they point to, one should set them to point to nullptr again. In this way, one can void double freeing the same buffer. How to use headers: Possibly put inline functions in files ending in.inl, the other declarations can go in the.h or.hpp files. Usually a problem in big projects is build time (try to compile Chromium from scratch). Possibly forward declare as often as possible. Always include the smallest amount of required headers. Precompile heavy headers. How to use namespaces: Possibly create namespaces for the different modules. I.e. namespace math, namespace graphics, etc. If you have something local to the translation unit, put it into an unnamed namespace. I.e. namespace GLuint GenerateBuffersAndFillRandomData ( int n ) GLuint b u f f e r ; // do a l l o f t h e i n i t i a l i z a t i o n and f i l l t h e random d a t a return b u f f e r ; // namespace namespace g r a p h i c s 6
// o t h e r s t u f f MyRandomEffect : : i n i t B u f f e r s ( ) mbuffer = GenerateBuffersAndFillRandomData ( m B u f f e r S i z e ) ; // namespace g r a p h i c s 4. Problem 4 One can use smart pointers so that some of the work around memory management is reduced. Still, there are many other issues to address regarding memory management. In some cases, a whole bunch of objects have the same life span. Consider a particle system: particles are alive for a given amount of time and afterwards their state gets restarted. There is often no need to create more particles. Once there is no longer need of the particle system, all of the particle objects have to be destroyed. Another example can be a parser in a compiler: it goes through all of the tokens generated by a lexer, it creates and updates new nodes and at the end we have an abstract syntax tree. All of the memory used by the nodes will have to be freed after compilation is done, but while the existence of the AST none of them have to be freed. The pattern which arises is that freeing of memory for all of the objects is done at once. Both examples would benefit from a pool allocator. This would require also overloading the new and delete operators in the corresponding classes. Some projects add their own macro for that and put it into class declaration. You can check here for a reference: https://github.com/google/angle/blob/master/src/compiler/translator/common.h On the other hand, in a particle system simulator there is often data parallelism to be exploited which calls for the use of some wide-type instruction sets like the AVX instruction set. Loading data into registers requires the data to be 32-byte alligned for AVX (unless you are using the instrinsics for unanligned data). Unfortunately, the standard allocator does not allow us to do that. So, if one wants to put all of the data into an std::vector, it would not be possible without passing our own aligned allocator. An example: https://gist.github.com/donny-dont/1471329 7