C++ Part I · Module 16 · Inheritance
Composition vs Inheritance
Six chapters have reached the same conclusion from six directions, and each time the alternative was mentioned rather than argued. A stack that inherited from a vector gained three operations it could not defend. A hierarchy that could satisfy the base's contract or the derived class's invariant but never both. A base class that lost its invariant to three lines of derived code. A derived overload that made every base overload of a name unreachable. In each case the fix was to hold a member instead of deriving, and that is the argument this chapter makes properly — as a procedure you can run rather than a preference you adopt. It covers what containment gives you that derivation does not, which is control over your own interface; what it costs, which is the part worth measuring because the objection is always the same; and the exactly two things only inheritance can do, so that the choice is made on capability rather than on habit. The cost objection is that forwarding functions must surely add a layer, and the answer is more decisive than expected: a call through a composed type's forwarding function and the same call on the inherited version compiled to the identical single instruction — a tail call to the same symbol. Not inlined cheaply. Gone.
Intermediate18 min readC++Part IFundamentalsInheritance
Part I · Module 16 · Chapter 16.7 · Composition vs Inheritance
1. The Two Shapes
Inheritance — the base part is inside your object and its interface becomes yours:
struct StackInherited : std::vector<int> {
void push(int v) { push_back(v); }
int pop() { int v = back(); pop_back(); return v; }
};Composition — the other object is a member and you publish only what you forward:
class StackComposed {
public:
void push(int v) { v_.push_back(v); }
int pop() { int x = v_.back(); v_.pop_back(); return x; }
bool empty() const { return v_.empty(); }
std::size_t size() const { return v_.size(); }
private:
std::vector<int> v_;
};Both hold a std::vector<int>. The difference is entirely about who controls the interface, and that is what the rest of the chapter is about.
2. What You Get Either Way
A. both support the intended interface:
inherited: size=2 pop=2
composed : size=2 pop=2Identical behaviour for the job the type exists to do. Chapter 16.1 §6 introduced this pair; here is the full comparison.
3. What Inheritance Also Gives Away
B. the inherited one ALSO supports things a stack should not:
s2[1] = 999 compiled; s2[1] is now 999
insert at begin() compiled; bottom is now 7, size=4
clear() compiled; size=0A write into the middle, an insertion at the bottom, and a wholesale clear — on a type whose entire purpose is last-in-first-out access. The composed version refuses all three:
D. the composed one REFUSES all three:
s[1] = 999 -> type 'StackComposed' does not provide a subscript operator
s.insert(s.begin(),7) -> no member named 'insert' in 'StackComposed'
s.clear() -> no member named 'clear' in 'StackComposed'With public inheritance, the base's entire public interface is part of your type's public interface. You do not get to choose which parts.
And this is not a one-time cost. Every operation added to std::vector in a future standard is automatically added to StackInherited, with no decision from you — which is the fragile base class exposure Chapter 16.8 §2 treats generally.
4. The Cost Objection, Measured
The standard objection is that forwarding adds a layer. Measured at -O2, the same call through both designs:
void viaInherited(SInh& s, const Rec& r) { s.push(r); } // push_back reached directly
void viaComposed (SComp& s, const Rec& r) { s.push(r); } // through a member + a forwardviaInherited : 1 instruction
b __ZNSt3__16vector<Rec>::push_back(const Rec&)
viaComposed : 1 instruction
b __ZNSt3__16vector<Rec>::push_back(const Rec&)
identical: True (1 vs 1 instructions)One instruction each — a tail call to the same symbol — and the sequences are identical. The forwarding function did not inline cheaply; it disappeared. There is nothing left of it in the generated code.
And the size, from Chapter 16.1 §7:
C. sizeof(StackInherited) = 24 ; sizeof(StackComposed) = 24Twenty-four bytes either way. No vtable (neither type has virtual functions), no padding, no extra pointer — the member is the vector, in the same storage the base subobject would have occupied.
So the honest accounting of composition's cost is: the forwarding functions you type, and nothing else. No space, no instructions, no indirection.
5. The One Case Where Inheritance Is Smaller
§4's "costs nothing" has one measured exception, and it is worth knowing both because it is real and because it explains a pattern you will see in the standard library.
An empty class — no data members at all, just behaviour — still occupies space as a member, because every object must have a distinct address:
struct LoggingPolicy { void note(const char*) const { /* no state */ } };
struct ComposedOne { LoggingPolicy p; long payload; }; // policy as a member
struct InheritedOne : LoggingPolicy { long payload; }; // policy as an empty basesizeof(LoggingPolicy) alone = 1 (an empty class is never 0)
A. EMPTY policy:
composed { LoggingPolicy p; long payload; } = 16
inherited : LoggingPolicy { long payload; } = 8 <-- empty base optimisation
B. STATEFUL member, for contrast:
composed = 16 inherited = 16Sixteen bytes composed, eight inherited — twice the size. As a member, the empty policy needs its own address, so it takes 1 byte and then 7 bytes of padding to align the long that follows (Chapter 9.8 §6 measured that arithmetic). As a base, the implementation is permitted to give it no storage at all, because a base subobject may share an address with the derived object — which Chapter 16.2 §2 measured happening.
That permission is the empty base optimisation, and it is why stateless policies, comparators and allocators are conventionally held as private base classes rather than members: std::vector's allocator and std::map's comparator are usually empty, and storing them as members would add a byte plus padding to every container.
The honest scope of the exception is narrow. Row B is the control: with a stateful member the sizes are identical again, 16 against 16. So this is not a general argument that inheritance is cheaper — it applies only when the thing you are holding has no data members at all, and it is worth about one word of padding in a small object.
And it does not change §7's procedure. If you reach for inheritance here it should be private inheritance — Chapter 16.3 §9 measured that private inheritance creates no substitutability, so you get the layout benefit without claiming an is-a relationship you do not mean. C++20's [[no_unique_address]] attribute gives the same saving on a member, which removes even this reason; it is outside this curriculum's C++17 baseline and worth knowing exists.
6. What Composition Gives You
Four things, in order of how much they matter.
You choose your interface. §3's three operations are the demonstration. Your type's public surface is exactly what you wrote.
You can change the member. Swap std::vector<int> for a fixed array or a deque and no caller notices, because no caller ever saw the vector. With inheritance the member type is your interface.
You can hold several. A type composed of three collaborators is ordinary; a type deriving from three base classes is a design discussion.
And the relationship is private. Nobody can write code that depends on your type being a vector, because nothing tells them it is.
(How you hold the member — by value, or as a unique_ptr when it must be polymorphic or optional — is Chapter 14.8 §6's table. By value is the default and the cheapest.)
7. The Decision Procedure
Run these in order and stop at the first that applies.
1. Does the type need to be substitutable for the other one? That is Chapter 16.3 §1's test — would every function taking the base still get what it was promised? If no, use composition and you are done. This answers most cases.
2. Does the type need to be stored in, or passed to, code that handles the base type generically? If yes, you need inheritance — §8.
3. Does the type need to change the other one's behaviour, not just use it? If yes, you need inheritance plus virtual functions — §8 again.
4. Otherwise, composition.
The ordering matters: step 1 is a question about meaning and steps 2 and 3 are questions about capability. Asking them in the other order is how you end up justifying a derivation after the fact.
8. The Two Things Only Inheritance Can Do
Being precise, because these are the whole case for the feature.
Substitutability. A composed type cannot be passed where the contained type is expected:
Inherited i; takesModel(i); -> "takesModel got id=1"
Composed c; takesModel(c); ->
error: no matching function for call to 'takesModel'
note: candidate function not viable: no known conversion from 'Composed'
to 'const Model' for 1st argumentNo known conversion. If a function takes const Model& and you need to pass your type, composition will not do it — and no amount of forwarding helps, because forwarding gives you members, not a type relationship. Chapter 16.3 §9 measured that only public inheritance creates the conversion at all.
And overridable behaviour. Inheritance lets a derived class supply an implementation that base-typed code will reach. That is the mechanism behind every pluggable component, and it is Module 17's subject — virtual functions and dynamic dispatch. Composition cannot do it: a member's functions are the member's, and a containing type cannot change what the member does internally.
So the case for inheritance is: you need one piece of code to work with several types through a common interface. If that sentence does not describe your situation, you wanted a member.
9. The Honest Costs of Composition
Being fair to the other side, because a one-sided argument is less useful.
You type the forwarding functions. For a wide interface this is real work and real lines of code. §4 measured that it costs nothing at run time, but it is not free to write or to read.
Forwarding can drift. A forwarder that adds a subtle behaviour change, or one that gets out of step with the member's semantics, is a defect inheritance would not have had — Chapter 15.3 §2's drift measurement, applied to a different relationship.
And deep forwarding chains get tedious. Three layers of composition each forwarding the same six functions is a signal that the layering is wrong, not that composition is.
Two mitigations worth knowing. Forward only what callers actually need — the exposure you avoid is the point, so a smaller surface is a feature rather than a shortcut. And where you genuinely want the whole interface of something you do not own, consider whether a free function taking the member would serve better than a member function at all.
10. What to Actually Do
Default to composition. Measured at zero runtime cost, so the default costs you nothing.
Run §7's procedure in order — meaning first, capability second.
Inherit only for substitutability or overridable behaviour — the two things nothing else does.
Remember the base's whole public interface becomes yours, including whatever is added to it later.
Forward only what callers need — a smaller surface is the point.
Hold the member by value unless it must be polymorphic or optional.
And treat deep forwarding chains as a layering problem, not an argument for deriving.
11. Mistakes Worth Making Once
Deriving to reuse members. Measured: three operations you cannot defend, at the same size.
Believing forwarding costs something. Measured: one instruction each, identical — a tail call to the same symbol.
Believing composition costs space. Measured: 24 bytes against 24.
Forgetting that the base's interface grows. Future additions to the base become yours automatically.
Trying to pass a composed type where the member's type is expected. Measured: no known conversion from 'Composed' to 'const Model'.
Expecting forwarding to give you substitutability. It gives members, not a type relationship.
Justifying a derivation after choosing it. §7's order exists to prevent exactly that.
Forwarding an entire wide interface by reflex. Forward what is needed; the rest is the benefit.
12. Interview Relevance
"When do you prefer composition?" By default — and the strong answer gives the procedure: unless the type must be substitutable for the other or must override its behaviour, hold a member.
"What does composition cost?" At run time, nothing. Measured: the forwarding call and the inherited call compiled to the identical single instruction, and both types to 24 bytes. The cost is the lines you type.
"What can inheritance do that composition cannot?" Two things: substitutability — measured as a hard no known conversion — and overridable behaviour through virtual functions.
"Why not inherit from std::vector to make a stack?" You inherit its whole interface, measured as three operations a stack must not have, and gain nothing in size or speed.
"Is 'prefer composition over inheritance' a trade-off?" Not really — there is no runtime column where inheritance wins. The asymmetry of mistakes is the real argument: a wrong member costs an afternoon, a wrong base class costs a migration.
"What are composition's honest downsides?" Forwarding code to write and read, forwarders that can drift, and tedium in deep layers.
13. What You Should Be Able to Say Now
- Inheritance makes the base's interface yours; composition publishes only what you forward.
- Measured: both designs supported the intended interface identically.
- Measured: the inherited stack also permitted a write into the middle, an insert at the bottom, and
clear(). - Measured: the composed one refused all three with clear diagnostics.
- You do not get to choose which parts of a public base's interface you expose — and additions to the base become yours later.
- Measured: a forwarding call and an inherited call compiled to one identical instruction — a tail call to the same symbol.
- The forwarding function disappeared, rather than merely inlining cheaply.
- Measured: 24 bytes against 24 — composition costs no space.
- So composition's only cost is the lines you type.
- Composition lets you choose your interface, change the member, hold several, and keep the relationship private.
- The procedure: substitutability needed? generic handling needed? behaviour change needed? otherwise compose.
- Meaning first, capability second — asking in the other order invites post-hoc justification.
- Measured: a composed type cannot be passed where the member's type is expected —
no known conversion. - Forwarding gives members, not a type relationship.
- Only inheritance gives substitutability and overridable behaviour — the second is Module 17's.
- The asymmetry decides borderline cases: a wrong member is an afternoon, a wrong base class is a migration.
14. Exercises
Exercise 1 — both shapes. Write a BoundedQueue holding at most 8 ints, once by deriving from std::vector<int> and once by containing one.
Exercise 2 — the exposure. List five operations the derived version permits that a bounded queue must not, and say what stops each in the composed version.
Exercise 3 — the cost. A colleague says the forwarding version adds a call layer. Give the measurement and explain what the compiler did.
Exercise 4 — the size. Predict both sizeof values and say why they are equal.
Exercise 5 — run the procedure. Apply §7's four steps to: a Logger and an std::ofstream; a WriteTransaction and a Transaction; a CachedModel and a ReferenceModel; a Matrix and a std::vector<double>; a FastModel and a ReferenceModel that must be interchangeable behind one name.
Exercise 6 — the hard refusal. Write the code that cannot compile with composition, give the diagnostic, and say why forwarding does not help.
Exercise 7 — the honest costs. Name composition's three downsides and a mitigation for each.
Exercise 8 — the asymmetry. Describe what it takes to fix a wrong member and a wrong base class, and say which you would rather have shipped.
15. Summary
- Inheritance hands the base's whole public interface to your callers; composition publishes only what you forward.
- Measured: both stacks did the job; the inherited one also permitted a middle write, a bottom insert, and
clear(), all of which the composed one refused. - Measured: the forwarding call and the inherited call compiled to one identical instruction — the forwarder disappeared.
- Measured: 24 bytes against 24. Composition's only cost is the lines you type.
- It buys interface control, a swappable member, several collaborators, and a private relationship.
- The procedure is meaning first, capability second — and a no at step one ends it.
- Only inheritance gives substitutability (measured as
no known conversion) and overridable behaviour (Module 17). - This is not a trade-off with two columns — and the asymmetry of mistakes settles whatever is left.
The argument is made, which leaves the chapter the module has been accumulating material for. Every preceding chapter produced a specific failure and a specific rule, and in practice the failures do not arrive labelled — they arrive as a hierarchy that has become hard to change, and the question is which of several recognisable patterns you are looking at. The last chapter of this module is that diagnostic catalogue: inheriting for reuse, a base class with no invariant, a hierarchy deep enough that no single level makes sense on its own, a derived class that narrows a promise, and the base class that keeps acquiring members because derived classes needed them. Each gets its symptoms, the measurement that confirms it, and the repair — which, in every case, turns out to be subtractive.
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.
