Use C++ delegating constructors to avoid code duplication

The C++11 delegating constructors reduce the code duplication and make effective use of the member initializer lists.

Tutorial | Aug 29, 2019 | hkumar 

Overview

It is reasonably common for C++ classes to have more than one constructor with different parameters. Let's take an example class Car. The class Car has some required fields: vin_, model_, and make_. Some of the Car's fields are optional and have their default values: year_, mileage_, and transmission_. There are three constructors in Car. One constructor takes only required fields, and the other two constructors take required and optional fields.

using std::string;

class Car {
public:
 // Constructor-1
 // Constructor with basic required fields.
 Car(const string& vin, const string& model,
    const string& make) 
    :vin_(vin),model_(model),make_(make) {
    allocateDynamicData();
 }

 // Constructor-2 
 // Constructor with required fields, mileage, and year
 Car(const string& vin, const string& model,
    const string& make,
    int year, int mileage) 
    :vin_(vin),model_(model),make_(make), 
       year_(year), mileage_(mileage) {
    allocateDynamicData();
 }

 // Constructor-3
 // Constructor with all fields
 Car(const string& vin,const string& model,
    const string& make,
    int year, int mileage,
    const string& transmission)
    :vin_(vin),model_(model),make_(make), 
       year_(year), mileage_(mileage), 
       transmission_(transmission) {
    allocateDynamicData();
 }

 ~Car() {
    /// de-allocates dynamic data
 }

private:

 void allocateDynamicData() {
     //.. allocate some dynamic data
 }

 string vin_, model_, make_;
 int year_{0}, mileage_{0};
 string transmission_{"Automatic"};
};

All the constructors use member initializer list and call a method to allocate some dynamic data. Overall, this code works fine and is efficient, but there is much duplication of code. The conventional approach to reduce this kind of code duplication is to move the common code to a member function, which can be called from all the constructors. Usually, that member function is called init.

The ubiquitous init() method

The modified class Car with init method looks like this:

using std::string;

class Car {
public:
 // Constructor-1
 // Constructor with basic required fields.
 Car(const string& vin, const string& model,
    const string& make) {
    init(vin, model, make, 0, 0, "Automatic");
 }

 // Constructor-2
 // Constructor with required fields, mileage, and year.
 Car(const string& vin,const string& model,
    const std::string& make,
    int year, int mileage) {
    init(vin, model, make, year, mileage, "Automatic");
 }

  // Constructor-3
 // Constructor with all fields
 Car(const string& vin,const string& model,
    const std::string& make,
    int year, int mileage,
    const std::string& transmission) {
    init(vin, model, make, year, mileage, transmission);
 }

 ~Car() {
    /// de-allocates dynamic data
 }

private:

 void init(const string& vin,const string& model,
          const string& make,int year,int mileage,
          const string& transmission) {

    vin_ = vin;
    model_ = model;
    make_ = make;
    year_ = year;
    mileage_ = mileage;
    transmission_ = transmission;

    allocateDynamicData();
 }

 void allocateDynamicData() {
    //.. allocate some dynamic data
 }

 string vin_, model_, make_;
 int year_, mileage_;
 string transmission_;
};

We have reduced the code duplication to a large extent. All three constructors invoke one member function, init, to do data member initialization and call allocateDynamicData. The private method init is written to initialize all the data members and can be easily modified to accept more parameters. Also note, that by initializing all the data members in init we preserve their initialization order. The readability and maintainability of code are improved.

However, reducing code duplication using the init method approach comes at a cost. For one, we had to forgo the performance benefit of the member initializer list. The string fields (e.g., make and model) were copy constructed in the member initializer list, but now they are default constructed and assigned to in the init method. Second, the private method init can be called inadvertently in a friend class (or within the same class) after the object is constructed. All in all, this approach has some obvious drawbacks, which are overcome by delegating constructors.

Delegating Constructors

Delegating constructors were added in the C++11 standard to address this issue of code duplication and verbosity in constructors. Delegating constructors is nothing but defining a constructor in terms of another constructor. In delegating constructors, a constructor that is called from another constructor is called the target constructor, and the calling constructor is called the delegating constructor. There can be any number of target constructors, and a delegating constructor can also be a target constructor, as shown below:

enter image description here

In our example case, the constructor that initializes all the data members of Car (Constructor-3) can be the target constructor for the Constructor-2 (the constructor with required fields, mileage, and year parameters), and the Constructor-2 can be the target constructor for Constructor-1 (the constructor with only required fields):

using std::string;

class Car {
public:
 // Constructor-1
 // Constructor with basic required fields
 // Delegating to Constructor-2
 Car(const string& vin, const string& model,
    const string& make)
:Car(vin, model, make, 0, 0)  {}

// Constructor-2
// Constructor with required fields, mileage, and year.
// Delegating to Constructor-3
Car(const string& vin, const string& model,
    const string& make,
    int year, int mileage)
:Car(vin, model, make,year, mileage, "Automatic") {}

 // Constructor-3
// Constructor with all fields
Car(const string& vin, const string& model,
    const string& make,
    int year, int mileage,
    const string& transmission)
:vin_(vin), model_(model), make_(make),
 year_(year), mileage_(mileage),
 transmission_(transmission) {
     allocateDynamicData();
}

~Car() {
    /// de-allocates dynamic data
}

private:

 void allocateDynamicData() {
    //.. allocate some dynamic data
 }

 string vin_, model_, make_;
 int year_, mileage_;
 string transmission_;
};

The above code with a delegating constructor is far more terse and readable than our first version. This code is more efficient than the init method approach because it is utilizing the member initializer list, and it is less error-prone. It is worth mentioning here that you cannot mix the call to a target constructor and initialize a data member in a member initializer list. If the target constructor is not initializing all the fields, then you must initialize the remaining fields in the body of the delegating constructor.

Further Reading 📖

Check out the following links to learn more about delegating constructors:

Delegating Constructors - MSDN

Delegating constructors - Stroustrup C++11 FAQ

2015 nextptr