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:

  1. The base subobject is constructed, in the order bases are listed.
  2. The members are constructed, in the order they are declared in the class — not in the order the initialiser list mentions them (Ch13.6).
  3. 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

7. Rules of Thumb