Ch16.2: Construction and Destruction Order
1. The Order Is Fixed, and It Is Not the Initialiser List
Building a derived object happens in three phases, and the order is fixed by the language:
- The base subobject is constructed, in the order bases are listed.
- The members are constructed, in the order they are declared in the class — not in the order the initialiser list mentions them (Ch13.6).
- The body of the derived constructor runs.
Destruction is exactly the reverse: body's scope ends, then members are destroyed in reverse declaration order, then the base. That is why a derived destructor may read the base's members: they are still alive. A base destructor must not touch anything the derived class added — those are already gone.
2. Initialising the Base
If the base has a default constructor, the derived class need not mention it. If it does not, the derived constructor must name it in the initialiser list:
class machine_part
{
protected:
::std::int_least32_t serial_{};
public:
machine_part() noexcept = default;
explicit machine_part(::std::int_least32_t s) noexcept : serial_{s} {}
};
class machine : public machine_part
{
::std::int_least32_t power_{};
public:
machine(::std::int_least32_t serial, ::std::int_least32_t power) noexcept
: machine_part{serial}, power_{power}
{
}
};
The base is named by its type, not by a member name:
machine_part{serial}. Writing serial_{serial} would be an
error — serial_ belongs to the base subobject, and base subobjects
are initialised through their constructor.
3. A Destructor Cannot Print
Ch6.16 said every function is
noexcept or throws, and destructors are
noexcept — always. Printing can fail (a closed pipe, a full disk), so
a destructor that prints is rejected by the compiler. This is what it says:
error: call to a ‘throws’ or ‘return_failure{E}’ function must be handled by an enclosing
‘try { } catch throws’, or the calling function needs a herbceptions specification
so the error can propagate
The message is precise: a noexcept function cannot call a
throws function without handling the failure itself. The diagnostic alone
is the lesson, but the fix matters too, so the example below records events into a
small buffer with a noexcept writer and prints the trace afterwards, from
main, where failing is allowed.
4. Watching the Order
namespace trace
{
inline ::std::string_view events[8]{};
inline ::std::size_t count{};
inline void add(::std::string_view e) noexcept
{
if (count != 8)
{
events[count++] = e;
}
}
} // namespace trace
struct base
{
base() throws { trace::add("base()"); }
~base() noexcept { trace::add("~base()"); }
};
struct engine
{
engine() throws { trace::add(" engine()"); }
~engine() noexcept { trace::add(" ~engine()"); }
};
struct machine : base
{
engine eng{};
::std::string_view tag{};
machine() throws : base{}, tag{"built"} { trace::add("machine()"); }
~machine() noexcept { trace::add("~machine()"); }
};
Note the two contracts: the constructors are throws because a derived
constructor that can fail must be allowed to propagate, and the destructors are
noexcept and therefore only touch a fixed array. If a base constructor
throws, the derived object never exists, and destructors run only for the parts that
were fully constructed.
5. The Whole Program
#include <fast_io.h>
#include <cstddef>
#include <string_view>
namespace trace
{
inline ::std::string_view events[8]{};
inline ::std::size_t count{};
inline void add(::std::string_view e) noexcept
{
if (count != 8)
{
events[count++] = e;
}
}
} // namespace trace
struct base
{
base() throws { trace::add("base()"); }
~base() noexcept { trace::add("~base()"); }
};
struct engine
{
engine() throws { trace::add(" engine()"); }
~engine() noexcept { trace::add(" ~engine()"); }
};
struct machine : base
{
engine eng{};
::std::string_view tag{};
machine() throws : base{}, tag{"built"} { trace::add("machine()"); }
~machine() noexcept { trace::add("~machine()"); }
};
int main()
{
try
{
{
machine m{};
::fast_io::io::println("in scope: ", m.tag);
}
::fast_io::io::println("out of scope");
for (::std::size_t i{}; i != trace::count; ++i)
{
::fast_io::io::println(trace::events[i]);
}
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
in scope: built
out of scope
base()
engine()
machine()
~machine()
~engine()
~base()
Read the bottom six lines as the two symmetric halves. Construction: base, then
members, then body. Destruction: body, then members in reverse, then base. The
tag member is constructed before machine() runs, which is
why the body can already rely on it.
6. Two Consequences People Get Wrong
- The derived body cannot help build the base. By the time
machine()'s body runs, the base is complete. Anything the base needs must be passed to it, through a constructor argument, a virtual call (never during construction — Chapter 18) or a setter used afterwards. - Destruction of a derived object never runs a derived destructor through a
base pointer unless the base destructor is
virtual. Deleting amachinethrough abase*without a virtual destructor is undefined behaviour. The rule for this chapter's non-virtual hierarchies is simple: do not own objects through base pointers.
7. Rules of Thumb
- Order the initialiser list in the same order as the declarations. It runs in declaration order, and a list that contradicts it reads as a lie.
- Name the base in the initialiser list whenever it has no default constructor, and prefer naming it always: it documents the dependency.
- A destructor is
noexcept: it may not allocate meaningfully, may not print, and may not throw. Record state and report it later. - A derived constructor that can fail is
throws; a base constructor that can fail makes every derived constructorthrows. - Store no pointer to a derived object in a base pointer unless the base has a
virtualdestructor (Ch16.7).