Thursday 20 March 2014

C++11 features - Improvement on object construction

1. Default value for member variables in class
This is really excellent improvement. Image in C++03 all the member variables with default values have to go to the initilization list. This is really a troublesome issue. A lot of code duplication and bad maintainability.

Problem in C++03:
class Foo {
   Foo() : x(1), y(1.0), z("Foo") {}
   Foo(int i) : x(i), y(1.0), z("Foo") {}
private:
   int x;
   double y;
   std::string z;
}.;

For all the constructors the member variables with default values has to go to the initialization list. Otherwise temporary objects will be created/deleted if any of them goes into constructor body or any member funciton. (More details please read my previous post: http://cpluspluslearning-petert.blogspot.co.uk/2014/03/the-order-of-object-initializationdestr.html)
Think about that a class has a few constructors, then all the default value initialization has to be copied into the initialization list for all constructor. Keeping all the default value the same will be hell-hot of job. By the way a lot of typing and at the same time have to keep them in the order in which they are declared in the class, if the initialization has dependency.
Maintainability is another issue. How to make sure all have the same default value when new developer coming and going, adding and removing constructor/code.

Duplication is evil.

Improvement in C++11:
class Foo {
   Foo() {}
   Foo(int i) : x(i){}
private:
   int x = 1;
   double y = 1.0
   std::string z = std::string("Foo");
}.;

Clean and tidy. If nothing appeared/initialized in the initialization list or in the construction body, the member variables, xy and z will be initialized as the default value when they are declared. For instance Foo() will take their default values because nothing appears in its initialization list and the constructor body is empty. Foo(int) will customize member variable x, but y and z will their default values.
Much easier to maintain. All the default value is defined when member variables are declared. They just have one place to go - where they are declared.

2. Delegate constructor and target constructor
Delegate constructor is referring to a constructor that delegates its object construction job into another constructor. Target constructor is referring to a constructor that is used by a delegate constructor to construct the object. A constructor can be delegate constructor and target constructor at the same time. But in one class there is at least one constructor is pure target constructor, otherwise the loop of delegation appears, which is the pitfall we should avoid. I will discuss this later.

Let's first illustrate the problem happening in C++03 and then we present the improvement/solutions coming from C++11

Problems in C++03:
The common code shared between constructors has to be duplicated either in all constructors or being lifted into a private function and called by all constructors. In such a workaround a few problems arise:
- If the common code coming from the initialization list, then the work around to lift them into a private function will create/delete temporary objects. Potentially performance damage.
- There is no limit to prevent the private function to accommodate the shared code from being called by other function.
- If the common code is duplicated in all constructors, then the maintainability will be a headache.

class Foo {
   Foo() : x(1.0), y(1.0), z(x + y) {}
   Foo(double xx) : x(xx), y(1.0), z(x+y) {}
   Foo(double xx, double yy) : x(xx), y(yy), z(x+y) {}
private:
   double x;
   double y;
   double z;
};
Workaround:
class Foo {
   Foo() {Init(1.0, 1.0);}
   Foo(double xx)  {Init(xx, 1.0)}
   Foo(double xx, double yy) {Init(xx, yy);}
private:
   void Init(double xx, double yy) {
       x = xx;
       y = yy;
       z = x + y;
   }
   double x;
   double y;
   double z;
};

In this workaround before hitting into Init(), x, y and z have been initialized to its default value. And they will be re-initialized again in Init(). This will not be cheap if member variables are big objects.

Improvement on C++11:
Rather than lift the common code into a private function, C++11 allows constructor to call another constructor in the initialization list, which is called delegation.

class Foo {
   Foo() : Foo(1.0) {}
   Foo(double xx) : Foo(xx, 1.0) {}
   Foo(double xx, double yy) : x(xx), y(yy), z(x+y) {}
   Foo(Foo& f) : Foo(f.x, f.y) {}
private:
   double x;
   double y;
   double z;
};

Foo() will delegate its construction work into Foo(double), which is to delegate its work to Foo(double, double). Here Foo() is a delegate constructor, Foo(double) is a delegate constructor and a target constructor and Foo(double, double) is a pure target constructor. And the copy constructor Foo(Foo&) is a delegate constructor as well. In this solution there is no private "Init()" function then hence no temporary objects, and there is no code duplication either.

Duplication is evil.

Keep in mind  a few things from C++11
- The target constructor's selection is based on the function overloading resolution and template argument deduction.
- An object life starts when coming out of the very first constructor. Any other delegate constructor can be taken as a private function working on a created object.
- Target constructor finishes before the delegate constructor, because the target constructor are called from initialization list. In the above example Foo(), the call graph:
   -> Foo()
        -> Foo(double)
            -> Foo(double, double) // the object life starts after coming out of this constructor
class Foo {
   Foo() : Foo(1.0) {std::cout << "Foo()" << std::endl;}
   Foo(double xx) : Foo(xx, 1.0) {std::cout << "Foo(double)" << std::endl;}
   Foo(double xx, double yy) : x(xx), y(yy), z(x+y) {std::cout << "Foo(double, double)" << std::endl;}
   Foo(Foo& f) : Foo(f.x, f.y) {}
private:
   double x, y, z
};
If we do:
void main(int argc, char* argv[])
{
    {Foo f;}
}
Output:
     Foo(double, double)
     Foo(double)
     Foo()

The object life starts immediately coming out of Foo(double, double). (When the target constructor finishes before its delegating constructor)
- Any exception thrown in the target constructor can be captured by its delegating constructor.
Foo(double i, double j) {throw ExceptionFooDD;)
Foo(double i) : try : Foo(i, 0.0){}
                      catch(...) (throw ExceptionFooD;)
Foo() : try :Foo(0.0){}
           catch (...) {throw ExceptionFoo;}
The stack unwinding can safely taken place upwards from target constructor up to delegate constructors.
(I am going to explain more on this "try block" in the next section)
- No other member variables can appeare in the initialization list if it is defined as a delegate constructor.
Foo() : F(1.0), y(1.0) {} // will generate compilation error, as y(1.0) can not appear in                                                                         //  initialization list any more
- No recursive loop in delegate constructors
Foo() : F(double) {}
Foo(double) : Foo(double, double) {}
Foo(double, double) : Foo() {}
In this case the recursive exists. The behavior is undefined and no diagnostics required for compilers as well according to C++11 standard. So it down to the programmer preventing this.
Tips: in one class there is at least one pure target constructor, which is not to delegate any work to any other constructor. Otherwise this could be good indication that there is a recursive loop.

3. Construction function try block
C++11 allows try block appearing in the initialization list to catch any exception from the initialization list of the member variables or target constructor. And the exception can be captured to take defense programming to make sure no resource leaked.

Problem in C++03:
class Bar{
   Bar(std::string fileName) {
       // open the file and allocate memory
       {
           throw BarCreationExcpeiton;// if something wrong with opening file or allocate memory
       }
   }
   ~Bar() {//release the resource}
private:
   File* m_Handler;
   int* m_Buffer;
};

class A {
   A(int i) {throw ACreationExcption;}
};

class Foo {
    Foo() : x(1.0) {}
Private:
    Bar m_Bar;
    A m_A;
    int x;
};

In C++03, the exception in A() constructor can not be captured in Foo() constructor, because it is thrown before it actually hitting into Foo's constructor body. This will cause any resource allocated in m_Bar will be lost out of control.

Improvement on C+11:
class Foo {
    Foo() : try : m_Bar("myFile"), m_A(0){}
                catch (...) {// house-keeping} {}
Private:
    Bar m_Bar;
    A m_A;
    int x;
};
Try block can come together with member variables in the initialization list. Catch any exception to get a chance to clean up.

4. Inherit default constructors
In C++11 it is not necessary to type all the constructors declaration and definition if the sub-class have the exactly same as its base class

Problem in C++03:
class Base {
public:
    Base(int, int);
    Base(int, std::string);
};

class Derived : public Base{
public:
    Derived(int i, int j) : Base(i, j){}                  
    Derived(int i, std::string s) : Base(i, s) {}
};
In C++03 even though Derived's constructors does no more than Base's constructors, the standard still requires to add that two line code for Derived's constructor. Otherwise C++03 standard will generate a default constructor Derived() for Derived class and complains about Derived() can not find its base-counterpart from Base class.

Improvement in C++11:
class Derived : public Base{
public:
    using Base::Base;
};

The line "using" will bring Base's constructors into scope and no more typing needed for Derived and at the same time will prevent compilers from generating default constructor for it. Keep in mind that this is a none-or-all feature.
(
Does this "using" remind you of something? The important usage of using in C++
- using namespace std:
- using Base::foo() // to bring the hidden name/function "foo" back to the scope of base's sub-class
}

Summary
Overall excellent to see all the features in C+11. You will not feel too overwhelmed if you are coming from Java world. But good to see these features introduced finally in C++11, which is to put C++ back into head-to-head race against other modern language like Java and Python.

Bibliography:
[1] http://www.stroustrup.com/C++11FAQ.html#delegating-ctor
[2] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n1986
[3] http://en.wikipedia.org/wiki/C++11#Explicit_conversion_operators

No comments:

Post a Comment