C++ / a working model

20 / 163   ·   C++11   ·   10 min

Construction and destruction order

Keep this sentence

The most-derived class initializes virtual bases first, then initializes direct bases and members in declaration order, and finally runs the constructor body. The written order of the initializer list cannot change these rules; destruction cleans up completed subobjects in reverse, and construction failure also relies on this determinate order.

In this lesson
  1. Order is determined by the class definition
  2. The most-derived class is responsible for virtual bases
  3. Reverse cleanup and failed construction
  4. Example
  5. Exercise

Order is determined by the class definition

A non-delegating constructor first initializes virtual bases. That order is determined by a depth-first, left-to-right traversal of the inheritance graph, and the same virtual base is initialized only once. Direct non-virtual bases are then initialized in base-specifier-list order, then non-static data members in class-body declaration order, and finally the constructor body runs.

Putting a member first in the initializer list does not make it construct first. If initialization of first_ depends on second_, second_ must be declared earlier in the class so that its initialization is already complete; otherwise a seemingly reasonable list order can still read an uninitialized value.

The most-derived class is responsible for virtual bases

In diamond inheritance, Left and Right both virtually inherit V, so a complete object D contains only one shared V subobject. When D is created, D's constructor supplies the initializer arguments for V; the V initializers written by Left and Right are ignored for this construction, and the related expressions are not evaluated.

When Left is constructed alone, Left is the most-derived class and its V initializer takes effect again. Write the initializer list in the order that actually executes, which helps readers judge dependencies and avoids compiler reordering warnings. A delegating constructor first fully executes the target constructor, then executes the delegating constructor body.

Reverse cleanup and failed construction

When D is destroyed normally, D's destructor body runs first, then members and direct non-virtual bases are destroyed in reverse, and finally the most-derived class destroys the virtual bases. The example records this path with a fixed-capacity log. It does not depend on addresses, object size, or other ABI details, and it does not allocate memory during destruction.

If an ordinary non-delegating constructor throws midway, the complete object that has not finished construction does not call its own destructor, but members and bases that have already completed construction are destroyed in reverse order. Therefore every resource should enter an RAII member as early as possible; a resource kept only in a raw pointer is not released automatically because outer construction failed.

Pitfalls

  • Assigning to a member in the constructor body is not initialization; the member-initialization phase has already finished before the body runs.
  • Virtual inheritance decides that a base subobject is shared; it does not make ordinary member functions virtual automatically.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <array>
#include <cassert>
#include <cstddef>

struct Log {
    std::array<int, 12> items{};
    std::size_t used = 0;
    void add(int n) noexcept { items[used++] = n; }
};
struct V {
    Log& log;
    V(Log& l, int id) : log(l) { log.add(id); }
    ~V() { log.add(-1); }
};
struct Left : virtual V {
    explicit Left(Log& l) : V(l, 99) { log.add(2); }
    ~Left() { log.add(-2); }
};
struct Right : virtual V {
    explicit Right(Log& l) : V(l, 98) { log.add(3); }
    ~Right() { log.add(-3); }
};
struct Member {
    Log& log;
    int id;
    Member(Log& l, int n) : log(l), id(n) { log.add(id); }
    ~Member() { log.add(-id); }
};
struct D : Left, Right {
    Member first, second;
    explicit D(Log& l)
        : V(l, 1), Left(l), Right(l), first(l, 4), second(l, 5) {
        log.add(6);
    }
    ~D() { log.add(-6); }
};

int main() {
    Log log;
    { D object(log); }
    const std::array<int, 12> expected{{1, 2, 3, 4, 5, 6, -6, -5, -4, -3, -2, -1}};
    assert(log.used == expected.size());
    assert(log.items == expected);
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-construction.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If second's constructor throws before finishing initialization, which D-related destructors run?

Show a reference answer

D's destructor body does not run, and second's own destructor does not run because it never finished construction. The already completed first, Right, Left, and the shared virtual base V are destroyed in that order. If second already has members that finished construction internally, those members are still destroyed by the cleanup of second's failed construction.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog