Understanding the Order of Construction & Destruction in Derived Classes

In C++, the process of constructing derived classes involves a systematic order that ensures proper initialization of each part of the class. This order is crucial, especially in cases where classes are derived from other classes, forming an inheritance chain.

Basic Concepts

Before delving into the order of construction, let's introduce two classes: Base and Derived.

class Base {
public:
    int m_id;

    Base(int id = 0) : m_id{id} {}
    
    int getId() const { return m_id; }
};

class Derived : public Base {
public:
    double m_cost;

    Derived(double cost = 0.0) : m_cost{cost} {}

    double getCost() const { return m_cost; }
};

In this example, Derived is derived from Base. It's essential to recognize that when constructing a derived class, the base class is not directly copied into the derived class. Instead, the derived class consists of both its own part (Derived) and the part inherited from the base class (Base).

1️⃣ Phase of Construction

When a derived class is inherited, a C++ follows a phased construction process. The most-base class (at the top of the inheritance tree) is constructed first. Then, each child class is constructed in order until the most-child class (at the bottom of the inheritance tree) is constructed last.

Let's demonstrate the construction order through a simple program.

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base class constructor called" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived class constructor called" << endl;
    }
};

int main() {
    Derived d;
    return 0;
}

The output of this program reveals the construction order:

Base class constructor called
Derived class constructor called

As expected, when constructing Derived, the Base portion is constructed first, followed by the Derived portion.

Key Points:

  • The base class constructor is always executed before the derived class constructor.
  • If no constructor is explicitly defined for the base class, the default constructor is called.

Order of Construction in Inheritance Chains

In real-world scenarios, classes are often part of inheritance chains, where a class is derived from another, which is derived from yet another forming a hierarchical structure.

class A {
public:
    A() { std::cout << "A\n"; }
};

class B : public A {
public:
    B() { std::cout << "B\n"; }
};

class C : public B {
public:
    C() { std::cout << "C\n"; }
};

class D : public C {
public:
    D() { std::cout << "D\n"; }
};

When constructing classes in an inheritance chain, the output illustrates the order:

Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D

Here, C++ first constructs class A, then proceeds down the chain through B, C, and finally D.

When an object of a derived class is instantiated, the constructors for all base classes are called before the constructor of the derived class. This process starts from the most distant base class (at the top of the hierarchy) and proceeds down to the immediate parent class, following a bottom-up approach.

Example:

Consider a simple inheritance chain:

#include <iostream>
using namespace std;

class A {
public:
    A() {
        cout << "Base class A constructor called" << endl;
    }
};

class B : public A {
public:
    B() {
        cout << "Derived class B constructor called" << endl;
    }
};

class C : public B {
public:
    C() {
        cout << "Most derived class C constructor called" << endl;
    }
};

int main() {
    C obj;
    return 0;
}

Output:

Base class A constructor called
Derived class B constructor called
Most derived class C constructor called
  • Class A is the most distant base class, so its constructor is called first.
  • Class B is derived from A, so its constructor is called after A's constructor.
  • Class C is derived from B, so its constructor is called last.

Key Points:

  • Constructors of base classes are always called before the constructor of the derived class.
  • The calling order is bottom-up in the hierarchy, starting with the most distant base class and moving towards the derived class.

The phases of construction can be summarized as follows:

  1. Base Class Constructors are Called First
  2. Member Variables are Initialized Next
  3. Derived Class Constructor is Called Last

1 Base Class Constructors are Called First (Bottom-up Construction)

When an object of a derived class is created, the constructors for all base classes are called before the constructor of the derived class. This ensures that the base part of the object is initialized before the derived part, following a bottom-up approach.

Example:
#include <iostream>
using namespace std;

class A {
public:
    A() {
        cout << "Base class A constructor called" << endl;
    }
};

class B : public A {
public:
    B() {
        cout << "Derived class B constructor called" << endl;
    }
};

class C : public B {
public:
    C() {
        cout << "Most derived class C constructor called" << endl;
    }
};

int main() {
    C obj;
    return 0;
}
Output:
Base class A constructor called
Derived class B constructor called
Most derived class C constructor called

In this example:

  • The constructor of Class A (the most distant base class) is called first.
  • Then, the constructor of Class B (the immediate base class) is called.
  • Finally, the constructor of Class C (the most derived class) is called.

Key Points:

  • The base class constructors are always called before the constructor of the derived class.
  • The order of construction moves bottom-up in the hierarchy, starting from the most distant base class and progressing to the most derived class.
  • Each base class constructor initializes the inherited part of the object.

2 Member Variables are Initialized Next

After the base class constructors have been called, the next phase involves the initialization of member variables. If the class has multiple member variables, they are initialized in the order they are declared in the class, regardless of the order they appear in the constructor’s initialization list.

Example:
#include <iostream>
using namespace std;

class A {
public:
    A() {
        cout << "Class A constructor" << endl;
    }
};

class B {
public:
    B() {
        cout << "Class B constructor" << endl;
    }
};

class C {
private:
    A a;  // Declared first, so initialized first
    B b;  // Declared second, so initialized second

public:
    C() : b(), a() {  // Initialization list order does not matter
        cout << "Class C constructor" << endl;
    }
};

int main() {
    C obj;
    return 0;
}
Output:
Class A constructor
Class B constructor
Class C constructor

Even though b() appears before a() in the constructor's initialization list, a is initialized first because it is declared before b.

Key Points:
  • Member variables are initialized after base class constructors are called.
  • The order of initialization of member variables follows the order in which they are declared in the class, not the order in the constructor's initialization list.
  • Each member is initialized before the derived class constructor is executed.

3 Derived Class Constructor is Called Last

After all base class constructors have been called and member variables have been initialized, the derived class constructor is executed. This is the final phase of the construction process, during which the derived class-specific members can be initialized or manipulated.

Example:
#include <iostream>
using namespace std;

class X {
public:
    X() {
        cout << "Class X constructor" << endl;
    }
};

class Y : public X {
public:
    Y() {
        cout << "Class Y constructor" << endl;
    }
};

class Z : public Y {
public:
    Z() {
        cout << "Class Z constructor" << endl;
    }
};

int main() {
    Z obj;
    return 0;
}
Output:
Class X constructor
Class Y constructor
Class Z constructor
  • The Class X constructor (base class) is called first.
  • Then, the Class Y constructor (derived from X) is called.
  • Finally, the Class Z constructor (most derived class) is called.
Key Points:
  • The constructor of the most derived class is called last, after all base class constructors and member variables have been initialized.
  • This allows the derived class constructor to safely assume that all inherited parts and member variables have already been initialized.

Special Cases in Construction Order

1 Virtual Base Classes

When dealing with virtual inheritance, the virtual base class is constructed only once, even if multiple derived classes inherit from it. The virtual base class constructor is called before any other non-virtual base classes.

Example:
class Base {
public:
    Base() {
        cout << "Base class constructor" << endl;
    }
};

class Derived1 : virtual public Base {
public:
    Derived1() {
        cout << "Derived1 constructor" << endl;
    }
};

class Derived2 : virtual public Base {
public:
    Derived2() {
        cout << "Derived2 constructor" << endl;
    }
};

class Final : public Derived1, public Derived2 {
public:
    Final() {
        cout << "Final class constructor" << endl;
    }
};

int main() {
    Final obj;
    return 0;
}
Output:
Base class constructor
Derived1 constructor
Derived2 constructor
Final class constructor

Here, the Base class constructor is called once, even though both Derived1 and Derived2 inherit from it via virtual inheritance.

2 Base Class with No Default Constructor

If a base class does not have a default constructor (i.e., it requires arguments), the derived class must explicitly call the base class constructor in the initialization list.

Example:
#include <iostream>
using namespace std;

class Base {
public:
    Base(int x) {
        cout << "Base class constructor called with " << x << endl;
    }
};

class Derived : public Base {
public:
    Derived(int y) : Base(y) {
        cout << "Derived class constructor called" << endl;
    }
};

int main() {
    Derived obj(5);
    return 0;
}

Output:
Base class constructor called with 5
Derived class constructor called

In this case, the derived class constructor must provide the argument for the base class constructor explicitly in the initialization list.

2️⃣ Phase of Destruction

When an object of a derived class is destroyed in C++, the destruction process follows a specific sequence, which is the reverse of the construction process. This ensures that all parts of the object are correctly cleaned up, including both the derived and base class members, as well as member variables. The phase of destruction is from the Most Derived class to Most Base class.

  1. Derived Class Destructor is Called First
  2. Member Variables are Destroyed Next
  3. Base Class Destructors are Called Last

1 Derived Class Destructor is Called First

When a derived class object is destroyed, the first destructor that is called is the one belonging to the most derived class. The derived class destructor performs cleanup specific to the derived class, including releasing any resources or memory that it might have allocated.

Example:

Consider the following C++ program:

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() {
        cout << "Base class destructor called" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived class destructor called" << endl;
    }
};

int main() {
    Derived d;
    return 0;
}
Output:
Derived class destructor called
Base class destructor called
  • When the Derived object d goes out of scope, the destructor of the derived class is called first.
  • After the derived class destructor finishes its work, the base class destructor is called.

Key Points:

  • The destructor of the derived class is always called first in the destruction process.
  • The destruction order ensures that any resources specific to the derived class are cleaned up before moving on to base class cleanup.

2 Member Variables are Destroyed Next

Once the derived class destructor finishes executing, the member variables of the derived class are destroyed. This happens in the reverse order of their declaration in the class. If there are multiple member variables, they are destroyed in the opposite order in which they were constructed.

Example:
class A {
public:
    A() { cout << "A constructor" << endl; }
    ~A() { cout << "A destructor" << endl; }
};

class B {
public:
    B() { cout << "B constructor" << endl; }
    ~B() { cout << "B destructor" << endl; }
};

class C {
private:
    A a;  // Declared first, destroyed last
    B b;  // Declared second, destroyed first

public:
    C() { cout << "C constructor" << endl; }
    ~C() { cout << "C destructor" << endl; }
};

int main() {
    C obj;
    return 0;
}
Output:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor
  • The members A and B are initialized in the order of declaration (first A, then B).
  • During destruction, the destructors are called in reverse order: first B, then A.

Key Points:

  • Member variables are destroyed after the derived class destructor finishes.
  • Destruction of member variables follows the reverse order of their construction.

3 Base Class Destructors are Called Last

After the member variables are destroyed, the base class destructor is called. If the inheritance chain involves multiple base classes, their destructors are called in the reverse order of construction, starting from the most immediate base class and working upwards to the most distant base class.

Example:

Consider a more complex inheritance chain:

#include <iostream>
using namespace std;

class X {
public:
    ~X() {
        cout << "Class X destructor" << endl;
    }
};

class Y : public X {
public:
    ~Y() {
        cout << "Class Y destructor" << endl;
    }
};

class Z : public Y {
public:
    ~Z() {
        cout << "Class Z destructor" << endl;
    }
};

int main() {
    Z obj;
    return 0;
}
Output:
Class Z destructor
Class Y destructor
Class X destructor
  • The destructor for the most derived class (Z) is called first.
  • Then the destructor for Y (the immediate base class) is called.
  • Finally, the destructor for the most distant base class (X) is called last.

Key Points:

  • Base class destructors are called last in the destruction sequence.
  • The destructors of base classes follow a top-down order (opposite of construction, which is bottom-up).

Special Cases in Destruction Order

Destruction Process in Virtual Inheritance

In the case of virtual inheritance, destruction follows the same reverse order principle, but the virtual base class destructor is called only once, and after all derived class destructors have been executed.

Example:
#include <iostream>
using namespace std;

class Base {
public:
    ~Base() {
        cout << "Base class destructor" << endl;
    }
};

class Derived1 : virtual public Base {
public:
    ~Derived1() {
        cout << "Derived1 destructor" << endl;
    }
};

class Derived2 : virtual public Base {
public:
    ~Derived2() {
        cout << "Derived2 destructor" << endl;
    }
};

class Final : public Derived1, public Derived2 {
public:
    ~Final() {
        cout << "Final class destructor" << endl;
    }
};

int main() {
    Final obj;
    return 0;
}
Output:
Final class destructor
Derived2 destructor
Derived1 destructor
Base class destructor
  • The Final class destructor is called first.
  • The destructors of Derived2 and Derived1 are called next.
  • The Base class destructor is called once, even though both Derived1 and Derived2 inherit from it.