IBM Z and LinuxONE - Languages - Group home

Introduction to the C++11 feature: delegating constructors

By FANG LU posted Tue March 24, 2020 07:39 PM

  

In C++98, if a class has multiple constructors, these constructors usually perform identical initialization steps before executing individual operations. In the worst scenario, the identical initialization steps are copied and pasted in every constructor. See the following example:


class A{

public:

  A(): num1(0), num2(0) {average=(num1+num2)/2;}

  A(int i): num1(i), num2(0) {average=(num1+num2)/2;}

  A(int i, int j): num1(i), num2(j) {average=(num1+num2)/2;}

private:

  int num1;

  int num2;

  int average;

};


These three constructors have the same function body. The duplicated codes make maintenance difficult. If you want to add more members or change the type of existing members, you have to make the same changes three times. To avoid code duplication, some programmers move the common initialization steps to a member function. The constructors achieve the same function by calling this member function. Let us revise the example as follows:


class A{

public:

  A(): num1(0), num2(0) {init();}

  A(int i): num1(i), num2(0) {init();}

  A(int i, int j): num1(i), num2(j) {init();}

private:

  int num1;

  int num2;

  int average;

  void init(){ average=(num1+num2)/2;};

};


This revision eliminates code duplication but it brings the following new problems:

  1. Other member functions might accidentally call init(), which causes unexpected results.
  2. After we enter a class member function, all the class members have already been constructed. It's too late to call member functions to do the construction work of class members.


C++11 proposed a new feature called delegating constructors to solve this existing problem. You can concentrate common initialization steps in a constructor, known as the target constructor. Other constructors can call the target constructor to do the initialization. These constructors are called delegating constructors. Let us use this new feature for the program above:


class A{

public:   

  A(): A(0){}

  A(int i): A(i, 0){}

  A(int i, int j) {

     num1=i;

     num2=j;

     average=(num1+num2)/2;

  }

private:

  int num1;

  int num2;

  int average;

};


You can see that delegating constructors make the program clear and simple. In this example, A() delegates to A(int i), so A(int i) is the target constructor of A(). A(int i) delegates to A(int i, int j), so A(int i, int j) is the target constructor of A(int i). Delegating and target constructors do not need special labels or disposals to be delegating or target constructors. They have the same interfaces as other constructors. As you haven seen from the example, a delegating constructor can be the target constructor of another delegating constructor, forming a delegating chain. Target constructors are chosen by overload resolution or template argument deduction. In the delegating process, delegating constructors get control back and do individual operations after their target constructors exit. See the following example:


#include <iostream>

using namespace std;

class A{

public:   

  A(): A(0){ cout << "In A()" << endl;}

  A(int i): A(i, 0){cout << "In A(int i)" << endl;}

  A(int i, int j){

     num1=i;

     num2=j;

     average=(num1+num2)/2;

     cout << "In A(int i, int j)" << endl;} 

private:

  int num1;

  int num2;

  int average;

};

int main(){

  class A a;

  return 0;

}

The output of this example is:

In A(int i, int j)

In A(int i)

In A()

Although we can use delegating chains in our program, we should avoid recursive calls of target constructors. For example:

class A{

public:   

  A(): A(0){}

  A(int i): A(i, 0){}

  A(int i, int j): A(){}

private:

  int num1;

  int num2;

  int average;

};


In the preceding example, a recursive chain of delegation exists in the program. The program is ill-formed.

One thing we need to keep in mind is that delegating constructors cannot have initializations of class members in their initializer lists; that is, a constructor cannot delegate and initialize at the same time. For example:


class A{

public:   

  A(): A(0), average(0){}

  A(int i): A(i, 0){}

  A(int i, int j) {

     num1=i;

     num2=j;

     average=(num1+num2)/2;

  }

private:

  int num1;

  int num2;

  int average;

};


In this example, A() delegates to A(int i) and initializes class member average at the same time, which is not allowed.

If an exception occurs in the body of a target constructor, it can be caught by the try block of the delegating constructor. The following example demonstrates this rule:


#include <iostream>

using namespace std;

class A{

public:

  A();

  A(int i);

  A(int i, int j);

private:

  int num1;

  int num2;

  int average;

};  

A:: A()try: A(0) {

  cout << "A() body"<< endl;

}

catch(...) {

  cout << "A() catch"<< endl;

}

A::A(int i) try : A(i, 0){

  cout << "A(int i) body"<< endl;

}

catch(...) {

  cout << "A(int i) catch"<< endl;

}

A::A(int i, int j) try {

  num1=i;

  num2=j;

  average=(num1+num2)/2;

  cout << "A(int i, int j) body"<< endl;

  throw 1;

}

catch(...) {

  cout << "A(int i, int j) catch"<< endl;

}

int main(){

  try{

     class A a;

     cout << "main body"<< endl;

  }

  catch(...){

     cout << "main catch"<< endl;

  }

  return 0;

}


The output of the example is:

A(int i, int j) body

A(int i, int j) catch

A(int i) catch

A() catch

main catch


As described, the delegating constructors feature can be easily understood and used. This feature helps reduce the code size and make your program more readable and maintainable.