Skip to content
VLSI Mentor

C++ Part I · Module 16 · Inheritance

Inheritance Mistakes That Produce Fragile Hierarchies

Every chapter in this module produced a specific failure and a specific rule, and in practice neither arrives labelled. What arrives is a hierarchy that has become hard to change — a base class nobody will touch because the blast radius is unknown, a derived class whose own safety checks turn out not to run, a chain of five classes where no single level describes anything. The useful skill at that point is diagnostic: recognising which of a small number of recurring shapes you are looking at, because each has a different repair. This chapter is that catalogue. Six shapes, each with the symptoms you would actually notice first, the measurement that confirms the diagnosis, and the fix. Two of them are measured here for the first time. One is a base class that acquires a public function and a data member in a later version, which this chapter measures giving every derived class a new operation it never approved and eight extra bytes, with no edit to the derived class and nothing for its author to review. The other is a derived class that tightens a rule its base deliberately left loose, which fails in both directions at once: called through a base reference its check is bypassed entirely, so the guarantee it exists to provide is void, and called directly it refuses input the base promised to accept. The pattern across all six is worth stating in advance, because it is the chapter's only general conclusion: every repair is subtractive.

Advanced19 min readC++Part IFundamentalsInheritance

Part I · Module 16 · Chapter 16.8 · Inheritance Mistakes That Produce Fragile Hierarchies

1. Six Shapes

They overlap in practice, and the value of separating them is that each has a different repair.

ShapeFirst symptom you notice
1. The fragile base class (§2)nobody will edit the base
2. A base class with no invariant (§3)the base is a bag of fields with setters
3. The hierarchy with no honest level (§4)casts to base and derived scattered about
4. A derived class that narrows a promise (§5)an override that validates, asserts, or throws
5. The base that keeps growing (§6)the base has members only one derived class uses
6. Inheriting for reuse (§7)the derived type has operations it cannot defend

Chapter 16.3 §1's test identifies most of these when applied before deriving. This chapter is for the hierarchies that already exist.

2. Shape One: The Fragile Base Class

The symptom: a base class that nobody wants to change, because no one can say what changing it will do.

The measurement. One source file, one derived class written against the base and never edited again, and a base that gains a public function and a data member in its next version:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Base {
        long payload = 0;
        void use() const {}
    #ifdef BASE_V2
        void reset() { payload = 0; }          // added in version 2
        long audit = 0;                        // and a new data member
    #endif
    };
    struct Derived : Base { void doWork() {} };   // never touched
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
V1: sizeof(Base)=8   sizeof(Derived)=8
    d.reset() -> error: no member named 'reset' in 'Derived'
V2: sizeof(Base)=16  sizeof(Derived)=16
    d.reset() -> COMPILES

Derived gained a public operation and eight bytes without being edited. d.reset() went from a compile error to valid code, and Derived's author was not consulted, did not review it, and has nothing in their file to review.

That is the fragility: a base class's public interface is also every derived class's public interface, so a base-class author cannot add anything without adding it everywhere. Chapter 16.7 §4 measured the static version of this exposure; this is the same fact over time.

The repair: shrink the base's public interface to what every derived class genuinely needs to expose, and move the rest behind protected (Chapter 16.4 §7) or out to a free function or a member. A base class with four public functions is changeable; one with forty is not.

3. Shape Two: A Base Class With No Invariant

The symptom: the base is a collection of fields with getters and setters, and you cannot state in one sentence what it guarantees.

Chapter 11.5 §2 required an invariant to be statable precisely. A base class without one has a specific consequence for a hierarchy: there is nothing for substitutability to be about. Chapter 16.3 §1's test asks whether a derived class can keep the base's promises, and if the base promises nothing, the test passes trivially — which sounds convenient and means the hierarchy is carrying no information.

The practical damage is that such a base attracts shape 5 (§6). With no invariant to protect, there is no principle for deciding what belongs in it, so members accumulate.

And Chapter 16.3 §6 measured the mutability version of this: an immutable Rectangle with no setters supports Square : Rectangle honestly, while adding a setter makes the same hierarchy wrong. So a base's invariant and its mutability together decide what can derive from it — adding a setter to a base class can invalidate every derivation below it, which is a change that looks local and is not.

The repair: either give the base a real invariant and the operations to maintain it, or accept that it is a plain aggregate and stop deriving from it. Chapter 11.1's distinction between a struct of data and a class with an invariant is the one to apply.

4. Shape Three: The Hierarchy With No Honest Level

The symptoms, and these are the most recognisable in a code review:

Casts between base and derived scattered through the code. Chapter 16.2 §8 noted that needing static_cast<Base&> occasionally is fine and that a scattering of them is a signal. What it signals is that code holding a base reference keeps needing to know the derived type — which means the base's interface is not the interface anyone actually uses.

A base class with no public interface, everything protected so derived classes can reach it. Chapter 16.4 §10 flagged this: a base whose entire surface is protected is not a type, it is a shared implementation detail with a derivation requirement attached.

And a level in the middle that describes nothing. If you cannot say what a MiddleThing is, in a sentence that does not mention its derived classes, it exists to hold code rather than to model anything.

The repair: collapse the levels that describe nothing, and move the shared code into a member or a free function. If the base has no public interface, it was never a base class — Chapter 16.7 §7's second question asks whether anything handles the base type generically, and the answer here is no.

5. Shape Four: A Derived Class That Narrows a Promise

The symptom: an override that validates, asserts, or throws where the base accepted the input.

The measurement. A scheduler whose contract documents negative values as meaningful, and a derived class that decides they are not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Scheduler {                        // contract: any n; negative means "as soon as possible"
        int last = 0;
        void schedule(int n) { last = n; }
    };
    struct StrictScheduler : Scheduler {      // refuses negatives
        void schedule(int n) {
            if (n < 0) throw std::invalid_argument("StrictScheduler: n must be >= 0");
            last = n;
        }
    };

    static void scheduleAsap(Scheduler& s) { s.schedule(-1); }   // legal per the base contract
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. on the type it was written for:
     scheduled, last=-1
B. on the derived type, through a Scheduler&:
     scheduled, last=-1
     (the base version ran -- non-virtual -- so last=-1)
C. and called through the derived type directly:
     THREW: StrictScheduler: n must be >= 0

It fails in both directions at once, which is the thing to notice.

Through a base reference (B), the derived class's check did not run. last=-1 — the exact value StrictScheduler exists to reject is now stored in it. Its guarantee is void, and the code that guarantee was written to protect is the code that bypassed it. Chapter 16.6 §1 is the mechanism: the base function was hidden, not overridden.

Called directly (C), it refuses what the base promised to accept. Any code written against Scheduler's documented contract is now broken for this type.

So there is no reading on which this works, and — exactly as Chapter 16.3 §5 found for Square — making schedule virtual does not fix it. It would only move the failure from B to C, so that the check always runs and the base's contract is always broken.

A derived class may weaken a precondition (accept more) and strengthen a postcondition (promise more). The reverse of either breaks substitutability.

The repair: the strictness belongs somewhere else. Either it is a property of the base — in which case put the check there and document it — or StrictScheduler is a different type that happens to schedule things, and should hold a Scheduler rather than be one.

6. Shape Five: The Base That Keeps Growing

The symptom: the base class has members that only one derived class uses, and a reviewer cannot tell which.

The mechanism is social rather than technical. A derived class needs something; adding it to the base is the shortest path; the base grows. Repeat for two years. Chapter 16.4 §4 measured the access half of this — a protected field is reachable three derivations down, so "the derived classes need it" is always locally true.

The result compounds with shape 1: every member added for one derived class is now in every derived class's interface and in the base's change-blast-radius. §2's measurement is what that costs each time.

Two diagnostic questions. For each member of the base, which derived classes use this? — if the answer is one, it is in the wrong place. And could a new derived class reasonably not want this? — if yes, the base is over-specified.

The repair: move each single-user member down into the derived class that uses it. This usually reveals that the base was the only thing holding two unrelated types together.

7. Shape Six: Inheriting for Reuse

Covered thoroughly already, so the short form.

The symptom: the derived type has operations it cannot defend. Chapter 16.1 §7 measured a stack that gained a write into the middle, an insert at the bottom, and clear().

The confirming question: Chapter 16.3 §1's test. If nobody ever wanted the derived type to be substitutable, the derivation was never doing modeling work.

The repair: composition — measured at 24 bytes against 24, with the forwarding call compiling to the same single instruction as the inherited one (Chapter 16.7 §4 and §5).

8. Deep Hierarchies

Depth is a symptom rather than a shape of its own, and worth a short section because it is the most visible one.

Three levels is unusual; five is almost always one of §2 through §6 applied repeatedly. The costs are concrete: construction runs Chapter 16.5's four phases at every level, so a five-deep object has twenty phases to reason about; a change at the top has the §2 blast radius multiplied by the number of levels; and hiding (Chapter 16.6 §3) can occur at any level, so working out which function a call reaches means checking five scopes.

The useful test is per level, not per hierarchy: for each level, state what it is in one sentence that does not mention the levels below it. The levels that fail are the ones to collapse.

9. The Symptom Table

Running it backwards, which is how you will actually use it:

What you noticeLikely shapeFirst thing to check
nobody will touch the base1 (§2)how many public members does it have?
the base is fields and setters2 (§3)can you state its invariant in a sentence?
casts to derived types all over3 (§4)does anything use the base's interface generically?
the base has no public members3 (§4)is it a type, or shared implementation?
an override that throws or asserts4 (§5)does the base's contract allow that input?
base members used by one subclass5 (§6)which derived classes use each member?
the derived type has odd operations6 (§7)was substitutability ever wanted?
five levels deepany, repeatedcan each level be described in one sentence?

10. Every Repair Is Subtractive

Collecting the repairs from §2 through §8:

  • shrink the base's public interface (§2)
  • give the base an invariant or stop deriving from it (§3)
  • collapse levels that describe nothing (§4)
  • move the strictness out of the derived class (§5)
  • move single-user members down out of the base (§6)
  • replace the derivation with a member (§7)
  • collapse the levels that cannot be described (§8)

Every one removes something. None adds a flag, a check, an override, or a level. That is worth noticing because the instinct when a hierarchy misbehaves is to add — and Chapter 16.3 §10 reached the same conclusion from the other direction, where the repair for a failed substitutability test was to make the base promise less.

11. What to Actually Do

Apply Chapter 16.3 §1's test before deriving — it prevents shapes 4 and 6 outright.

Keep a base class's public interface small — it is every derived class's interface, now and later.

Give a base a statable invariant, or stop deriving from it.

Describe every level in one sentence that does not mention the levels below.

Treat an override that validates, asserts or throws as a failed derivation.

Ask which derived classes use each base member — one means it is in the wrong place.

Read scattered base/derived casts as a diagnosis, not a nuisance.

And expect every repair to remove something. If your fix adds a level or a flag, you have treated a symptom.

12. Mistakes Worth Making Once

Adding a public member to a base "just for one subclass". Measured: every derived class gained it and 8 bytes, with no edit and nothing to review.

Deriving from a base with no invariant. Substitutability has nothing to be about.

Adding a setter to a base class. It can invalidate every derivation below it — Chapter 16.3 §6.

An override that throws on input the base accepts. Measured: bypassed through a base reference, breaking the base's contract when called directly.

Expecting virtual to fix a narrowed promise. It moves the failure from one side to the other.

Scattering casts to work around a base interface nobody uses.

A base class that is entirely protected. That is shared implementation, not a type.

Five levels. Twenty construction phases and five scopes to check for hiding.

Fixing a hierarchy by adding. Every repair in §10 removes.

Assuming a tool will warn. Seven findings across two modules say otherwise.

13. Interview Relevance

"What makes a base class fragile?" Its public interface is every derived class's interface, so it cannot change without changing them. Measured: a derived class gained a function and 8 bytes without being edited.

"What is wrong with a base class that has no invariant?" There is nothing for substitutability to be about, and nothing to decide what belongs in the base — so it accumulates members.

"What does an override that throws tell you?" That the derived class narrows a precondition, which breaks substitutability. The strong answer notes it fails both ways: bypassed through a base reference, contract-breaking when called directly.

"Why doesn't virtual help there?" It only chooses which side breaks.

"What are the symptoms of a hierarchy that needs collapsing?" Casts between base and derived scattered about, a base with no public interface, and levels you cannot describe in a sentence.

"How do you fix a fragile hierarchy?" By removing — shrink the base, collapse levels, move members down, replace derivation with containment. Noticing that every repair is subtractive is the answer that stands out.

"How do you avoid all of this?" Chapter 16.3's test, applied before writing : Base.

14. What You Should Be Able to Say Now

  1. Six recurring shapes, each with a different repair, and most preventable by one test applied early.
  2. Measured, shape 1: a base gained a public function and a data member; Derived went from 8 to 16 bytes and d.reset() from no member named 'reset' to compiling — with no edit to Derived.
  3. A base's public interface is every derived class's public interface, over time as well as statically.
  4. A base with no invariant gives substitutability nothing to be about and attracts members.
  5. Adding a setter to a base can invalidate every derivation below it — Chapter 16.3 §6's mutability finding.
  6. Scattered base/derived casts mean the base's interface is not the one anyone uses.
  7. A base class that is entirely protected is shared implementation, not a type.
  8. A level you cannot describe without mentioning its subclasses exists to hold code.
  9. Measured, shape 4: through a Scheduler& the strict check was bypassed (last=-1); called directly it threw.
  10. So narrowing fails both ways — the derived guarantee is void, or the base contract is broken.
  11. virtual would only choose which side breaks, exactly as in Chapter 16.3 §5.
  12. A derived class may accept more and promise more; never less of either.
  13. A base member used by one derived class is in the wrong place.
  14. Depth multiplies everything — four construction phases per level, and one scope per level to check for hiding.
  15. Every repair in this chapter removes something, because the only available lever is to make the base promise less.
  16. No tool reports any of these — the seventh such finding across Modules 15 and 16.

15. Exercises

Exercise 1 — name the shape. For each symptom, give the shape and the first thing you would check: nobody edits the base; an override asserts; four casts to a derived type in one function; a base with twelve public setters; a base member used by one subclass.

Exercise 2 — the blast radius. A base gains one public function. Say what happens to every derived class, what the derived authors see, and what warns.

Exercise 3 — the setter. Explain how adding a setter to a base class can invalidate a derivation that was previously correct.

Exercise 4 — predict the narrowing. A base accepts any string; a derived class rejects empty ones. Give the behaviour through a base reference and through the derived type, and say which party is wrong in each.

Exercise 5 — why not virtual. Explain in two sentences why making the function virtual does not repair exercise 4.

Exercise 6 — the per-level sentence. For a five-level hierarchy of your own or your codebase's, write one sentence per level without mentioning lower levels. Say which levels you could not write.

Exercise 7 — which derived classes use it? Take a base class with six members and answer the question for each. Say what you would move.

Exercise 8 — subtractive repairs. For three of the shapes, give the repair and say explicitly what is removed.

Exercise 9 — prevent them. State Chapter 16.3 §1's test and say which of the six shapes it catches before any code exists.

16. Summary

  • Six recurring shapes, recognisable from symptoms, each with a different repair.
  • Measured: a base gaining a public function and a field took Derived from 8 to 16 bytes and made d.reset() compile — no edit to Derived, nothing to review.
  • A base's public interface is every derived class's interface, which is what makes a wide base unchangeable.
  • A base with no invariant gives substitutability nothing to be about; adding a setter can invalidate every derivation below it.
  • Scattered casts, an all-protected base, and an undescribable level all mean the hierarchy has levels that hold code rather than model anything.
  • Measured: a narrowing derived class was bypassed through a base reference (last=-1) and threw when called directly — broken both ways, and virtual only chooses which.
  • A derived class may accept more and promise more, never less.
  • Every repair is subtractive, because the only lever is to make the base promise less.
  • None of it warns — the seventh such finding across two modules.

That closes Module 16, and with it inheritance as a modeling decision. The module has deliberately shown a hierarchy doing almost nothing: a derived object has a base part at a known offset, a derivation asserts substitutability, construction runs in four phases per level, and a same-named member hides rather than replaces. Every mistake in this chapter was available at that level of mechanism, which was the point of holding the rest back — the designs that go wrong go wrong before anything interesting happens at run time.

What the hierarchy cannot yet do is the thing hierarchies exist for. A function taking a ReferenceModel& currently calls ReferenceModel's functions, whatever object it was handed — Chapter 16.3 measured exactly that, and Chapter 16.6 explained why. So there is still no way to write one piece of code that drives several implementations chosen at run time, which is the basis of every pluggable component, every swappable model, and every interface a verification environment is built around. Module 17 is Runtime Polymorphism: ten chapters on virtual functions and how a call finds the right implementation, override and final as the keywords that turn a signature mistake into a compile error, abstract classes as the way a contract is stated in C++, the honest cost of a virtual call measured rather than assumed, and — in its last two chapters — how to design an interface worth implementing and how to write the code that consumes one without knowing which implementation it was handed. It also includes the three failures the module's own blurb names, two of which this module has been carefully setting up: the missing virtual destructor, whose consequence Module 10's leak measurements have already prepared you to recognise, and object slicing, which is Chapter 16.2's by-value measurement becoming dangerous the moment the discarded part matters.

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.