C++ Part I · Module 16 · Inheritance
Construction and Destruction Through a Hierarchy
Chapter 16.2 established that a derived object is one object with two parts, and that picture makes an ordering unavoidable: the base part has to be a valid object before any derived code can rely on it, so it must be built first and torn down last. That much is guessable. What is worth measuring is that it is not two phases but four, because both classes can have members of their own, and members are built before the constructor body that uses them — so a derived object comes into existence in a fixed sequence of base members, base body, derived members, derived body, and goes out of existence through all four in exact reverse. This chapter measures that sequence with an instrumented member in each class, then covers the one piece of syntax it forces: a derived constructor initialises its base in the member initializer list, and if the base has no default constructor and you forget, the compiler produces one of the clearer error messages in C++. Then two consequences of the ordering that are easy to get wrong. For the entire time the base constructor is running, the derived part of the object does not exist yet — which is fine until a base constructor tries to use something only the derived class provides. And if construction fails partway through, exactly the parts that were finished are destroyed, which this chapter measures by throwing from a derived constructor body and watching which destructors run.
Intermediate17 min readC++Part IFundamentalsInheritance
Part I · Module 16 · Chapter 16.5 · Construction and Destruction Through a Hierarchy
1. Why There Has to Be an Order
Chapter 16.2 §3's picture does the work. A derived object is one object containing a complete base subobject, and a constructor's job is to turn storage into a valid object.
So the base part must be constructed before the derived constructor body runs, for a simple reason: the derived constructor is allowed to call base member functions, read base members, and generally treat the base part as a working object. That is only true if it already is one.
Base first on the way in, base last on the way out. Everything else in this chapter is detail.
Chapter 12.8 established the order within one class — members in declaration order, then the body, and the exact reverse on destruction. This chapter is that rule extended across a derivation.
2. Four Phases, Measured
Instrument both classes with a member that announces itself:
struct Member {
const char* tag;
explicit Member(const char* t) : tag(t) { std::printf(" + member %s\n", tag); }
~Member() { std::printf(" - member %s\n", tag); }
};
struct Base {
Member bm{"of Base"};
int v;
explicit Base(int x) : v(x) { std::printf(" + Base body (v=%d)\n", v); }
~Base() { std::printf(" - Base body\n"); }
};
struct Derived : Base {
Member dm{"of Derived"};
int w;
explicit Derived(int x, int y) : Base(x), w(y) { std::printf(" + Derived body (w=%d)\n", w); }
~Derived() { std::printf(" - Derived body\n"); }
};A. constructing a Derived:
+ member of Base
+ Base body (v=1)
+ member of Derived
+ Derived body (w=2)
B. fully constructed; now destroying:
- Derived body
- member of Derived
- Base body
- member of Base
C. doneFour phases, and destruction is the exact reverse of all four. The phrase "base before derived" is true but too coarse: the base's members precede the base's body, which precedes the derived class's members, which precede the derived body.
| Phase | Construction | Destruction |
|---|---|---|
| 1 | base's members | derived body |
| 2 | base's constructor body | derived's members |
| 3 | derived's members | base's destructor body |
| 4 | derived's constructor body | base's members |
The pattern generalises: at every level, members before body; and the whole sequence reverses on teardown. Chapter 12.8's reversal is what makes nested construction safe, and it holds across derivation boundaries unchanged.
3. Initialising the Base
A derived constructor names its base in the member initializer list, before any members:
explicit Derived(int x, int y) : Base(x), w(y) { /* ... */ }Base(x) is not a function call in the ordinary sense — it is the base's initialiser, and it belongs first in the list because Chapter 12.3 established that the list's written order does not control the order things happen, but writing it in the real order avoids confusing the reader. For a base the convention is absolute: the base goes first.
Three notes.
You may name only your direct base, not a grandparent. Each level initialises the level immediately above it.
If you write nothing, the base's default constructor runs. That is usually what you want for a base with sensible defaults, and §4 is what happens when there isn't one.
And using Base::Base; inherits the base's constructors, which Chapter 16.4 §3's listings used. It is the right tool when a derived class adds behaviour but no new state, and it saves forwarding constructors by hand.
One thing the compiler does help with here. Chapter 12.1 §6 established that the written order of a member initializer list does not control the order anything happens. Writing the base out of order is therefore harmless to behaviour, and clang says so anyway:
explicit Derived(int x, int y) : w(y), Base(x) { /* ... */ } // base written secondwarning: field 'w' will be initialized after base 'Base' [-Wreorder-ctor]
| ^~~~ ~~~~~~~
| Base(x) w(y) + Base v=1
+ Derived w=2The base still ran first, exactly as §2 measured — the written order changed nothing. But -Wreorder-ctor flagged the mismatch and even printed the corrected order. This is a pleasant exception to the pattern in Chapters 15 and 16, where the interesting failures draw no diagnostic: here the compiler notices a cosmetic discrepancy and tells you how to fix it. Take the advice, because a list whose written order matches the real one is the only kind a reader can trust.
4. The Diagnostic Worth Recognising
Omit the base initialiser when the base has no default constructor:
struct Base { int v; explicit Base(int x) : v(x) {} };
struct Derived : Base { int w; explicit Derived(int y) : w(y) {} }; // Base(int) not callederror: constructor for 'Derived' must explicitly initialize the base class 'Base'
which does not have a default constructor
note: 'Base' declared hereThat message says exactly what to do, which is rarer than it should be. The fix is : Base(something), w(y).
The deeper point is that the compiler will not let you skip the base. There is no path on which a derived object exists with an unconstructed base part — Chapter 12.1 §1's guarantee — that unlike C's allocate-then-fill-in-the-fields, there is no path on which a field goes unset — applied to the base subobject.
5. What the Base Constructor Can See
struct Base {
int baseVal;
explicit Base(int x) : baseVal(x) {
std::printf(" Base ctor: baseVal=%d\n", baseVal);
report(); // a member function, during base construction
}
void report() const { std::printf(" Base::report sees baseVal=%d\n", baseVal); }
};A. constructing a Derived:
Base ctor: baseVal=1
Base::report sees baseVal=1
Derived ctor: derivedVal=2A base constructor may safely use its own members and call its own member functions. By the time the body runs, phase 1 is complete: every base member is initialised. report() saw baseVal=1 correctly.
6. What It Cannot See
The symmetric fact, and the one that catches people:
While the base constructor is running, the derived part of the object does not exist yet.
Phase 3 has not happened. The storage for the derived members is there — it is one object — but nothing has initialised it, so reading a derived member from a base constructor would be reading an uninitialised object, which Chapter 3.8 §4 established is undefined rather than merely unpredictable.
C. ... after full construction, tag=0xABCDThe derived member is correct after construction completes, and only then.
The practical consequence is a design rule: a base class constructor must not depend on anything a derived class supplies. If a base needs a value only the derived class knows, that value is a constructor parameter the derived class passes up —
explicit Derived(int x, int y) : Base(x), derivedVal(y) {}— not something the base reaches down for.
(The famous form of this hazard is calling a virtual function from a base constructor, where the call does not reach the derived override because the derived part is not yet established. That is Module 17's subject — 17.2 for the dispatch mechanism, and 17.9 for the interface design that avoids needing it. The ordering fact above is the whole reason, and it is available now: there is nothing for a derived override to operate on.)
7. When Construction Fails Partway
Throw from the derived constructor body, after the base and a derived member are built:
struct Derived : Base {
Member m;
Derived() { std::printf(" (Derived body, about to throw)\n"); throw std::runtime_error("boom"); }
~Derived() { std::printf(" - Derived\n"); }
}; + Base
+ Member
(Derived body, about to throw)
- Member
- Base
caught: boomRead the destructor lines carefully. - Member and - Base ran. - Derived did not.
The rule is exact and worth stating: whatever was fully constructed is destroyed; the object whose constructor did not complete has no destructor call. The Derived object never came into existence, so ~Derived is not called — but the base subobject and the member did come into existence, so theirs are.
Chapter 12.7 §8 measured this for a throwing constructor within one class; this is the same rule with a base in the picture. It is also why Chapter 12.9's RAII works at all: each completed piece cleans itself up, with no bookkeeping.
The design consequence: if a derived constructor can throw, every resource acquired before the throw must already be owned by something with a destructor. A unique_ptr member is safe; a raw pointer assigned in the body is not, because ~Derived will not run to release it.
8. The Order Is Not Negotiable
A short list of things people try:
You cannot construct the derived part first. There is no syntax for it and no reason to want it — the base is the part everything else relies on.
You cannot skip the base. §4's diagnostic.
You cannot initialise a grandparent directly. Each level initialises its own direct base.
And you cannot change the destruction order. It is the reverse of construction, always, which is what makes it safe to rely on.
What you can control is which base constructor runs, and what you pass it — which is all the control the ordering needs.
9. What to Actually Do
Expect four phases, and the exact reverse on destruction.
Initialise the base first in the member initializer list — : Base(x), members....
Pass a base what it needs as a constructor argument; never let it reach into the derived part.
Use using Base::Base; when a derived class adds behaviour but no state.
Assume the derived part does not exist during base construction — because it does not.
Make every resource acquired in a constructor owned by a member with a destructor, so a throw cannot leak.
And read §4's diagnostic as the instruction it is — name the base.
10. Mistakes Worth Making Once
Thinking it is two phases. Measured: four, with members before bodies at each level.
Forgetting the base initialiser for a base with no default constructor. The diagnostic names the fix.
Trying to initialise a grandparent. Each level initialises its direct base only.
A base constructor using something the derived class supplies. The derived part is uninitialised storage at that point.
Expecting ~Derived to run after a throw from Derived's body. Measured: it does not — only - Member and - Base ran.
Acquiring a raw resource in a constructor body that can throw. ~Derived will not release it.
Assuming the member-initializer-list's written order controls anything. Chapter 12.3's rule — declaration order wins; write it in real order for the reader.
Expecting to control destruction order. It is the reverse of construction, always.
11. Interview Relevance
"What order does a derived object get constructed in?" Base's members, base's body, derived's members, derived's body — four phases — and destruction is the exact reverse. The four-phase answer distinguishes.
"How does a derived constructor initialise its base?" In the member initializer list, first, by naming the base. If the base has no default constructor and you omit it, the compiler says so explicitly.
"Can a base constructor call a derived class's function?" No usefully — the derived part is not initialised yet. The strong answer gives the ordering reason rather than citing a rule.
"What does a base need from its derived class?" Nothing. Whatever it needs comes in as a constructor parameter.
"If a derived constructor throws, which destructors run?" The base's and the completed members' — not the derived class's, because the derived object was never constructed. Measured.
"Why does that make RAII work?" Each completed piece cleans itself up with no bookkeeping.
12. What You Should Be Able to Say Now
- The order is forced by the layout — the base part is what everything else relies on.
- Measured: four phases — base's members, base's body, derived's members, derived's body.
- Measured: destruction reverses all four — derived body, derived members, base body, base members.
- Members precede bodies at every level, which is Chapter 12.8's rule extended across a derivation.
- A derived constructor initialises its base in the member initializer list, written first.
- You may name only your direct base.
- Measured diagnostic:
must explicitly initialize the base class 'Base' which does not have a default constructor. - There is no path on which a derived object exists with an unconstructed base.
using Base::Base;inherits the base's constructors.- Measured: a base constructor may safely use its own members —
Base::report sees baseVal=1. - While the base constructor runs, the derived part does not exist yet — storage, not an object.
- So a base must take what it needs as a constructor parameter, never reach downward.
- Measured on a throw from the derived body:
- Memberand- Baseran;- Deriveddid not. - Whatever was fully constructed is destroyed; an object whose constructor did not complete has no destructor call.
- Hence a resource acquired in a constructor body must be owned by a member with a destructor.
- The order is not negotiable — and the only control you have is which base constructor runs and what you pass it.
13. Exercises
Exercise 1 — predict the trace. A Base with one instrumented member and a Derived with two. Give every construction and destruction line in order.
Exercise 2 — the four phases. Fill in the table for construction and destruction, and say why each ordering is forced.
Exercise 3 — initialise it. Given struct Base { explicit Base(int, const char*); }, write a Derived constructor taking three arguments that initialises the base correctly.
Exercise 4 — the diagnostic. Say what the compiler reports when the base initialiser is omitted, and what the fix is.
Exercise 5 — the dependency direction. A base needs a buffer size that only the derived class knows. Give the wrong design and the right one.
Exercise 6 — the uninitialised window. Explain why reading a derived member from a base constructor is undefined, in terms of the four phases.
Exercise 7 — the throw. A derived constructor acquires a raw new in its body and then throws. Say which destructors run, what leaks, and how to fix it with one change.
Exercise 8 — inherited constructors. Say when using Base::Base; is the right tool and when it is not.
14. Summary
- The order follows from the layout: base first in, base last out.
- Measured: four phases — base's members, base's body, derived's members, derived's body — with destruction the exact reverse.
- A derived constructor names its base first in the member initializer list, and may name only its direct base.
- Measured diagnostic when the base is omitted and has no default constructor: the message names the fix.
- Measured: a base constructor may safely use its own members; the derived part does not exist yet while it runs.
- So a base takes what it needs as a parameter and never reaches into its derived classes.
- Measured on a throw from the derived body: the base and the completed member were destroyed, the derived destructor did not run.
- Whatever was fully constructed is destroyed — which is why RAII survives a failed construction with no cleanup code.
Construction is settled, and with it every mechanical question about what a derived object is. One behavioural question remains, and Chapter 16.3 ran into it without stopping to explain it: in the non-virtual run, calling setWidth through a Rectangle& reached the base's function even though the object was a Square with a function of the same name. That is not dispatch and it is not overriding — the derived function did not replace the base one, it hid it, and hiding is a name-lookup rule with consequences that surprise people in a different way. The next chapter measures the sharp version: a derived class that adds one overload of a name makes every base overload of that name unreachable, so a call that had an exact match in the base either stops compiling or, worse, silently converts its argument and reaches the derived function instead. Both outcomes are measured, including the one-line fix.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
