Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 43

Constructor and Destructor Order in Complex Hierarchies

Recommended resources

Hello! Welcome to your next lesson in the "Modern Embedded C++" module.

In our last session, we went deep into the mechanics of C++ polymorphism, dissecting the v-table and v-pointer, and quantifying the memory and performance overhead of virtual functions. We concluded with the critical rule of using virtual destructors for polymorphic base classes to prevent resource leaks. That discussion naturally leads to the broader topic of an object's entire lifecycle.

Today, we'll trace the life of an object from birth to death, focusing on the precise order of events. Understanding this sequence is not just academic; it's fundamental to writing correct, bug-free C++ and is a frequent topic in senior-level interviews. A mistake in constructor or destructor logic can lead to subtle bugs, from resource leaks to undefined behavior.

Your learning outcome is to: Trace constructor and destructor execution order in complex inheritance hierarchies, including static object initialization.


1. The Fundamental Rule: Construction and Destruction Order

Let's start with the foundational principle that governs object lifecycle in an inheritance hierarchy.

  • Construction: An object is built from the "inside out." The base class parts are constructed first, followed by the derived class parts. Think of it as laying the foundation before building the house.
  • Destruction: An object is torn down from the "outside in," in the exact reverse order of construction. The derived class destructor runs first, followed by the base class destructors.

This principle extends to any level of inheritance.

C++ Constructor and Destructor Call Sequence in Inheritance
This image illustrates the order of constructor and destructor calls for simple, single, multiple, and multi-level inheritance. For any hierarchy, constructors are called from the top-most base class down to the most-derived class. Destructors are always invoked in the reverse order.

In multiple inheritance, the base classes are constructed in the order they are listed in the class declaration, not the order they appear in the member initializer list. This is a common interview "gotcha."




#include <iostream>

class Base1 { public: Base1() { std::cout << "Base1 constructor\n"; } };
class Base2 { public: Base2() { std::cout << "Base2 constructor\n"; } };

// Base2 is listed first in the initializer list, but Base1 is first in the declaration.
class Derived : public Base1, public Base2 {
public:
    Derived() : Base2(), Base1() { 
        std::cout << "Derived constructor\n"; 
    }
};

int main() {
    Derived d;
}

// Output:
// Base1 constructor
// Base2 constructor
// Derived constructor

2. The Role of the V-Pointer in Construction

Connecting back to our previous lesson, a crucial question is: when does the virtual mechanism become active? The v-pointer (vptr) is set up during construction, but it's a phased process.

  1. The memory for the entire object is allocated.
  2. The base class constructor runs. Upon entry to the base constructor, the vptr is set to point to the base class's v-table.
  3. The derived class constructor runs. Upon entry, the vptr is updated to point to the derived class's v-table.

This has a critical consequence: inside a constructor or destructor, the object's dynamic type is that of the class being constructed/destructed. A virtual function call inside a Base constructor will always resolve to Base::function(), never a derived version.

C++ Under the Hood: Internal Class Mechanisms - Chris Ryan - CppCon 2024

This CppCon talk provides an excellent visualization of this process. The presenter uses an 'onion' analogy to explain how an object is built in layers.

Watch the segment from 02:27 to 11:29. Focus on these key points: The 'inside-out' construction and 'outside-in' destruction analogy. How the vptr is set at the beginning of each constructor's body, effectively changing the object's dynamic type as it's being built. The step-by-step trace showing which v-table is active at each stage of construction and destruction.


3. Complications: Virtual Inheritance and the "Dreaded Diamond"

Multiple inheritance can lead to a situation known as the "dreaded diamond," where a class inherits from the same base class through multiple paths.

class Vehicle { /* ... */ };
class LandVehicle : public Vehicle { /* ... */ };
class WaterVehicle : public Vehicle { /* ... */ };
class AmphibiousVehicle : public LandVehicle, public WaterVehicle { /* ... */ };

In this setup, AmphibiousVehicle would contain two separate Vehicle subobjects, leading to ambiguity. C++ solves this with virtual inheritance.

Multiple and Virtual Inheritance, C++ FAQ

The C++ FAQ provides a definitive explanation of this problem and its solution.

Read the sections 'What is the “dreaded diamond”?' and 'Where in a hierarchy should I use virtual inheritance?'. This will explain the ambiguity and how the virtual keyword on the intermediate classes (LandVehicle and WaterVehicle) resolves it by ensuring only one Vehicle subobject.

Virtual inheritance changes the construction rules significantly.

Rule for Virtual Base Classes: The constructor for a virtual base class is called by the most-derived class's constructor. In our example, AmphibiousVehicle is directly responsible for constructing Vehicle, bypassing the intermediate classes.

Multiple and Virtual Inheritance, C++ FAQ

Let's continue with the C++ FAQ to understand these special rules for construction and destruction.

Read the three sections starting with 'What special considerations do I need to know about...' through 'What is the exact order of destructors...'. These sections provide the precise rules for constructor and destructor ordering in the presence of virtual inheritance.

The full order of construction is:

  1. Virtual base classes (in depth-first, left-to-right order).
  2. Non-virtual base classes (in declaration order).
  3. Member variables (in declaration order).
  4. The constructor body of the class itself.

Destruction, again, is the exact reverse.


4. Before main(): Static Object Initialization

So far, we've discussed objects created within a function's scope. What about objects with static storage duration (globals, file-scope statics, function-local statics)? Their constructors must run before main() is ever called.

This leads to a classic C++ problem: the Static Initialization Order Fiasco (SIOF). The C++ standard guarantees that static objects within a single translation unit (a single .cpp file) are initialized in order of definition. However, it does not guarantee any specific initialization order between different translation units.

If a global object g_consumer in consumer.cpp depends on another global object g_provider in provider.cpp, you have a 50/50 chance that g_provider hasn't been constructed when g_consumer's constructor is called.

C++ Weekly - Ep 382 - The Static Initialization Order Fiasco and C++20's constinit

This video from C++ Weekly with Jason Turner is the canonical explanation of the SIOF. It demonstrates the problem and discusses modern solutions.

Watch from 00:52 to 06:38, and then the recap from 11:01 to 12:28. Pay attention to: How simply changing the linking order of .cpp files triggers the bug. The core issue: dependency between static objects in different translation units. The recap summarizing the problem clearly.

The Solution: Construct on First Use

The most common and robust solution to the SIOF is the "Construct on First Use" idiom, which leverages a function-local static object.

// provider.h
class Provider;
Provider& get_provider(); // Access via a function

// provider.cpp
Provider& get_provider() {
    static Provider instance; // Guaranteed to be constructed only once,
                              // the first time this function is called.
    return instance;
}

// consumer.cpp
Consumer::Consumer() {
    // Now it's safe. get_provider() will ensure the instance exists.
    std::string data = get_provider().provide(); 
    consume(data);
}

This works because the C++ standard guarantees that function-local statics are initialized the first time control passes through their declaration. This idiom effectively turns a static initialization problem into a runtime-ordered one.


5. Under the Hood: The .init_array

How does the system physically execute these constructors before main? For embedded engineers who work close to the metal, understanding the link-level mechanism is crucial.

In modern ELF-based systems (like Linux and most embedded toolchains), the compiler and linker work together:

  1. For each global/static object that needs construction, the compiler generates a small initialization function.
  2. The linker gathers pointers to all these initialization functions and places them into a special array in a dedicated section of the executable file called .init_array.
  3. The C runtime startup code (which runs before main) iterates through this array, calling each function pointer. This ensures all static constructors are executed.

For destruction, a similar mechanism exists. When a static object's constructor runs, it typically registers its destructor with a runtime function like __cxa_atexit. This function adds the destructor to a list of functions to be called when the program exits normally.

C/C++ Runtime Startup - Ethereal Wake

This article, 'C/C++ Runtime Startup', provides a fantastic low-level view of this process.

This is a detailed read. Focus on these parts: ELF Initialization Sections: Read this section to understand what .init_array, .fini_array, and .preinit_array are. Note the C code example that shows the runtime iterating through the array. C++ ABI Extensions (Destruction): This is very important. Read to understand why .fini_array is not sufficient for C++ and how __cxa_atexit is used to register destructors for objects that have been successfully constructed. Freestanding Implementation: Skim this C code example. As an embedded engineer, this shows you exactly how you would implement this yourself on a bare-metal system: a simple loop over the __init_array_start and __init_array_end symbols provided by the linker.



Conclusion

You now have a complete picture of an object's lifecycle in C++, from simple inheritance to complex virtual hierarchies and static initialization. This knowledge is not just theoretical; it's essential for debugging, resource management, and understanding system startup behavior.

Key Takeaways:

  • Order of Operations: Construction is always base-to-derived; destruction is always derived-to-base.
  • V-Pointer State: The vptr is updated as construction proceeds up the inheritance chain. Inside a base class constructor, the object behaves like a base class object.
  • Virtual Inheritance: This resolves the "dreaded diamond" but alters the rules: the most-derived class is responsible for constructing the virtual base(s).
  • Static Initialization: Static objects are constructed before main. The order is only guaranteed within a single translation unit. Dependencies between units can lead to the Static Initialization Order Fiasco (SIOF).
  • SIOF Solution: The "Construct on First Use" idiom, using a function-local static, is the standard C++ solution.
  • Low-Level Mechanism: The linker populates an array of function pointers (.init_array) that the C runtime executes before calling main. Destructors are registered via __cxa_atexit.

Preview of the Next Lesson:

We have now covered the core object-oriented machinery of C++. Next, we will turn our attention to other modern C++ features that are critical for writing safe, expressive, and efficient embedded code. Our next lesson will be: Apply constexpr for compile-time computation and nullptr for type-safe null pointers in embedded C++ code.