Skip to content
VLSI Mentor

C++ Part I · Module 16 · Inheritance

public Inheritance and the Is-A Relationship

Chapter 16.1 stated that public inheritance asserts substitutability — that every derived object is usable wherever a base object is expected, by code that has never heard of the derived type. Stating a claim is not the same as having a way to check one, and the check is what decides every design decision in this module, so this chapter turns the claim into a test and then applies it to the hierarchy every textbook uses as the warning. A Square is a Rectangle: the sentence is true in English, the derivation compiles, and every member makes sense. What this chapter measures is what happens when ordinary code written against Rectangle receives a Square — and the measurement is more interesting than the usual telling, because it was run twice. With plain member functions, the function's contract is satisfied and the Square quietly stops being square. With virtual functions, so that the derived class's own code runs, the Square stays square and the function's stated promise is broken instead. Two runs, two different failures, and the useful observation is that neither is the right answer: you can honour the base class's contract or the derived class's invariant, and there is no version of this hierarchy that does both. That is the conclusion worth carrying, because it says the defect was never in the mechanism. It was in the claim, which was false before anybody chose how to dispatch a call.

Intermediate18 min readC++Part IFundamentalsInheritance

Part I · Module 16 · Chapter 16.3 · public Inheritance and the Is-A Relationship

1. The Claim, Turned Into a Test

Chapter 16.1 §3 stated the claim. Here it is as something you can check:

For every function that takes a Base, does passing a Derived still satisfy everything that function was promised?

The important words are every function and was promised. Not "does it compile" — Chapter 16.1 §4 measured that it compiles, and compiling is not the question. The question is whether the base's documented behaviour survives.

This is usually called the Liskov substitution principle, and the version worth remembering is shorter: a derived class may add, never remove or weaken. It may do more than the base promises. It must not do less, and it must not refuse something the base accepted.

2. The Canonical Example

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Rectangle {
        int w = 1, h = 1;
        void setWidth (int x) { w = x; }
        void setHeight(int y) { h = y; }
        int area() const { return w * h; }
    };

Rectangle's interface makes a promise that nobody writes down because it seems too obvious to state: width and height are independent. You can set either without affecting the other.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Square : Rectangle {
        void setWidth (int x) { w = x; h = x; }   // must keep w == h
        void setHeight(int y) { w = y; h = y; }
    };

Square has an invariant of its own — w == h — which Chapter 11.5 §2 would require to hold for the object's whole lifetime. And "a square is a rectangle" is true in geometry, so the derivation looks unarguable.

Now ordinary code, written against Rectangle, knowing nothing about Square:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void resizeTo3x4(Rectangle& r)
    {
        r.setWidth(3);
        r.setHeight(4);
    }

Its contract: after this returns, r is 3 wide and 4 tall, so area() is 12. Its only assumption is the one Rectangle advertises.

3. Run One: Plain Member Functions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. the function on the type it was written for:
     Rectangle -> w=3 h=4 area=12   (expected 3x4=12)
B. the SAME function on a Square, passed as a Rectangle&:
     Square    -> w=3 h=4 area=12   (expected 3x4=12)

The function kept its promise. area() is 12, exactly as documented, for both types.

And the Square is now 3 wide and 4 tall. Its invariant — the single thing that makes it a Square rather than a Rectangle — is gone. The object still has the type Square, still has Square's member functions, and no longer satisfies Square's defining property.

Why did Square's careful setters not run? Because they were never called:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. and note WHICH setWidth ran:
     via Square&    : w=3 h=3
     via Rectangle& : w=3 h=1   <-- the base version ran

Same object, same call, different behaviour depending on the static type of the reference. Through a Square& the derived version ran and kept w == h. Through a Rectangle& the base version ran. The functions are not virtual, so the compiler chose by the reference's type — which is name hiding, and Chapter 16.6 is the chapter on the mechanism. The consequence here is enough: Square's invariant-maintaining code is simply bypassed by any code holding a base reference.

4. Run Two: Virtual Member Functions

So make the setters virtual, so the derived versions actually run. (virtual and override are Module 17's subject — 17.2 and 17.3. They appear here purely as an instrument, to answer the obvious question of whether the mechanism rescues the design.)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Rectangle {
        int w = 1, h = 1;
        virtual ~Rectangle() = default;
        virtual void setWidth (int x) { w = x; }
        virtual void setHeight(int y) { h = y; }
        int area() const { return w * h; }
    };
    struct Square : Rectangle {
        void setWidth (int x) override { w = x; h = x; }
        void setHeight(int y) override { w = y; h = y; }
    };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
with VIRTUAL setters, so Square's versions actually run:
     Rectangle -> w=3 h=4 area=12
     Square    -> w=4 h=4 area=16   <-- the function promised 12
     Square's own invariant (w==h) holds: 1

Square's invariant holds, and the function's promise is broken. resizeTo3x4 documented that area() would be 12. It is 16. The function did nothing wrong; it called two documented operations in order, and a type it had never heard of redefined what they mean.

5. Neither Run Is Correct

Put the two side by side, because this is the chapter's point:

area()Square's invariant w == h
plain member functions12 — contract keptbroken — 3×4
virtual member functions16 — contract brokenholds — 4×4

There is no third option. The two requirements are in direct conflict: the base's interface says the dimensions are independent, and the derived class's invariant says they are equal. Honouring one violates the other, and the choice of dispatch mechanism only decides which one breaks.

So the common framing — "this is why you need virtual functions" — is wrong, and measurably so. virtual moved the failure; it did not remove it.

6. What Actually Went Wrong

The derivation asserted that a Square is usable wherever a Rectangle is expected. Apply §1's test honestly:

What was Rectangle promised? That setWidth changes the width and nothing else.

Can Square keep that? No. Square cannot change its width without changing its height, because then it would not be a square. It must either break its own invariant or break the promise.

Square cannot do everything Rectangle promises. So it is not substitutable for a Rectangle, and public inheritance was the wrong tool — before any question of dispatch arose.

And notice the shape of the error, because it recurs. The English sentence "a square is a rectangle" is about values — the set of squares is a subset of the set of rectangles. The C++ claim is about objects with mutable state and an interface. A mutable Rectangle offers an operation a Square cannot support, so the subset relation on values does not survive the addition of a setter.

Which means the same hierarchy can be fine or broken depending on mutability. An immutable Rectangle with no setters — construct it, read area() — has no operation Square cannot support, and Square : Rectangle passes the test cleanly. Chapter 16.8 §3 returns to this, because "add a setter later and the hierarchy becomes wrong" is a real way for a design to rot.

7. The Test, Stated Properly

Four questions, in the order they are worth asking:

Can the derived class support every base operation, with the base's documented meaning? If any operation must be weakened, refused, or redefined, stop.

Does it strengthen any precondition? A derived class that accepts less than the base is not substitutable — Chapter 16.8 §5 measures one.

Does it weaken any postcondition? A derived class that promises less on return is not substitutable either.

And does it preserve every invariant the base's users rely on? Including unstated ones like "these two fields are independent", which is the one §2's example violates.

A practical shortcut that catches most failures: write down the base's promises as sentences, then try to say each one about the derived class. If any sentence becomes false or needs a qualifier, you have your answer. The cost of this is five minutes and it is the only check available, because Chapter 16.1's callout established that the compiler cannot have an opinion here.

8. Hierarchies That Fail the Test

HierarchyWhich promise cannot be kept
Square : Rectangledimensions are independent
Penguin : Bird with fly()every Bird can fly()
ReadOnlyFile : File with write()every File can be written
Stack : std::vectorall of vector's operations — Chapter 16.1 §7 measured three
ImmutableList : List with add()every List can be added to
FixedBuffer : Buffer with resize()every Buffer can be resized
Set : std::vectorduplicates are preserved; order is insertion order

The pattern in every row: the base offers an operation the derived class must refuse or redefine. A derived class that needs to disable something is the clearest possible signal, and if you find yourself overriding a function to throw, or to do nothing, or to assert, the test has already failed.

9. The Other Two Kinds of Inheritance

C++ has three, and only one of them makes §1's claim. Measured by asking whether the derived-to-base pointer conversion exists at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
is_convertible<D*, Base*> -- i.e. does substitutability hold?
     public    : 1
     protected : 0
     private   : 0

Only public inheritance gives substitutability. With a private or protected base, the conversion does not exist, so no base-reference parameter can accept your object — the relationship is invisible outside the class. And the base's own public members become inaccessible from outside:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
protected base -> 'ping' is a protected member of 'Base'
private   base -> 'ping' is a private member of 'Base'

Private inheritance therefore means "implemented in terms of" rather than "is a" — which is what composition means, with the differences that you get access to the base's protected members and can override its virtual functions. It is a narrow tool; Chapter 16.7 argues that a member is almost always better.

And the defaults are a trap worth measuring:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class  D : Base  -> convertible? 0     (class default = private)
struct D : Base  -> convertible? 1     (struct default = public)

class D : Base is private inheritance. Someone writing that and expecting substitutability gets a confusing error at the first base-reference call rather than at the declaration. Chapter 16.2 §10's advice stands: write public explicitly, every time.

10. What To Do When the Test Fails

Three options, in order of how often they are right.

Make it a member. You wanted the behaviour, not the relationship — Chapter 16.7 is the decision procedure and Chapter 16.1 §7 measured that it costs nothing.

Find the real common base. If Square and Rectangle genuinely share something, it is an immutable Shape with area() and no setters. Both can be substituted for that, because it promises nothing either of them must refuse. Moving the common part up until every derived class can honour it is the standard repair, and it usually means the base gets smaller.

Or keep them unrelated. Two types that both have a width and a height are not thereby related. This is the option people resist and it is frequently correct.

11. What to Actually Do

Apply §1's test before writing : Base — would every function taking the base still get what it was promised?

Write the base's promises down as sentences and try to say each about the derived class.

Treat "I need to disable this operation" as a failed test, not a design problem to solve.

Do not expect virtual to rescue a false claim — measured, it moves the failure.

When the test fails, make it a member, shrink the base, or keep them unrelated.

Remember that mutability decides some cases — an immutable base may be substitutable where a mutable one is not.

And always write public explicitly — class D : Base is private inheritance, measured.

12. Mistakes Worth Making Once

Deriving because the English sentence is true. "A square is a rectangle" is about values; C++ is about objects with operations.

Treating "it compiles" as substitutability. Chapter 16.1 §4 measured that it compiles regardless.

Believing virtual fixes it. Measured: contract 12 / invariant broken, versus contract broken / invariant held.

Overriding a function to throw or do nothing. The clearest signal of a failed test.

Forgetting unstated promises like "these fields are independent".

Strengthening a precondition in a derived class. Accepting less than the base is not substitutable.

Assuming class D : Base is public. Measured: is_convertible is 0.

Using private inheritance where a member would do. You get a narrow tool and a confusing relationship.

Adding to a base to make a hierarchy work. The repair is usually to remove.

13. Interview Relevance

"What does public inheritance mean?" That every derived object is substitutable for a base object by code that knows only the base. The answer that leads with the promise rather than the syntax is the strong one.

"Explain the Square/Rectangle problem." Rectangle promises independent dimensions and Square cannot support that. The answer that distinguishes it is: measured both ways, plain functions keep the contract and break the invariant, virtual functions do the reverse — so virtual does not fix it. Very few candidates have run it.

"What is the Liskov substitution principle, in one line?" A derived class may add, never remove or weaken.

"How do you check a hierarchy?" Write the base's promises as sentences and try to say each one about the derived class.

"What does it mean if you need to override something to throw?" The derivation is wrong — that is a refused operation, which substitutability forbids.

"What is private inheritance for?" "Implemented in terms of" rather than "is a" — measured, the derived-to-base conversion does not exist. A member is usually better.

"Is class D : B public or private?" Private. struct defaults to public. Measured.

14. What You Should Be Able to Say Now

  1. The test: for every function taking a Base, does passing a Derived still satisfy everything that function was promised?
  2. A derived class may add, never remove or weaken.
  3. Rectangle promises that width and height are independent, which is the unstated promise Square cannot keep.
  4. Measured, plain member functions: area() = 12 (contract kept) and the Square became 3×4 (invariant broken).
  5. Measured: through a Square& the derived setter ran (w=3 h=3); through a Rectangle& the base version ran (w=3 h=1) — same object, different behaviour by static type.
  6. Measured, virtual member functions: area() = 16 (contract broken) and the invariant held.
  7. Neither run is correct — you can keep the base's contract or the derived invariant, never both.
  8. So virtual relocates the failure rather than fixing it.
  9. The defect is in the claim, which was false before dispatch was chosen.
  10. The English sentence is about values; the C++ claim is about objects with operations — which is why a setter breaks it.
  11. Mutability can decide the case — an immutable Rectangle with no setters would pass.
  12. A derived class that must disable an operation has already failed the test.
  13. Measured: is_convertible<D*, Base*> is 1 for public, 0 for protected, 0 for private — only public inheritance gives substitutability.
  14. Measured: class D : Base is private (convertible 0); struct D : Base is public (1).
  15. Private inheritance means "implemented in terms of", and a member is almost always better.
  16. The repairs are to shrink the base, hold a member, or keep the types unrelated — all subtractive.

15. Exercises

Exercise 1 — state the test. Write §1's test in your own words, in one sentence, without the word "inherit".

Exercise 2 — the unstated promise. Name the promise Rectangle makes that is not written anywhere, and say how you would have found it.

Exercise 3 — predict both runs. For resizeTo3x4 on a Square, give w, h and area() with plain setters and with virtual setters, and say which requirement breaks in each.

Exercise 4 — the dispatch question. Explain why virtual cannot fix this hierarchy, in two sentences.

Exercise 5 — judge six. Pass or fail with the broken promise named: Penguin : Bird; ReadOnlyFile : File; WriteTransaction : Transaction; FixedBuffer : Buffer; CachedModel : ReferenceModel; Celsius : Temperature.

Exercise 6 — mutability. Show a version of Rectangle for which Square : Rectangle passes the test, and say exactly what you removed.

Exercise 7 — the three kinds. Give is_convertible<D*, Base*> for public, protected and private bases, and say what each kind means.

Exercise 8 — the default. A colleague writes class Fast : ReferenceModel and gets an error passing a Fast& to a function taking ReferenceModel&. Diagnose it.

Exercise 9 — repair one. Take a failing hierarchy of your own and apply all three repairs from §10, then say which you would ship.

16. Summary

  • The test: does every function taking the base still get what it was promised when handed a derived object? A derived class may add, never remove or weaken.
  • Measured with plain member functions: the contract held (area() = 12) and the Square became 3×4 — its invariant gone.
  • Measured: the base setter ran through a base reference (w=3 h=1) while the derived one ran through a derived reference — same object, different behaviour.
  • Measured with virtual member functions: the invariant held and the contract broke — area() = 16 where 12 was promised.
  • Neither run is correct, so virtual relocates the failure rather than fixing it — the defect was in the claim.
  • The English sentence is about values; C++ is about objects with operations, which is why adding a setter breaks it. An immutable base would pass.
  • An override that disables, asserts or throws means the test already failed.
  • Measured: only public inheritance gives substitutability — is_convertible 1, 0, 0 — and class D : Base is private.
  • The repairs are subtractive: shrink the base, hold a member, or keep the types unrelated.

The test is in place and the failure mode is understood, which leaves the access question the previous two chapters kept deferring. Chapter 16.2 measured that a derived class can reach its base's protected members and not its private ones, and treated that as a fact about syntax. It is really a design decision, and the next chapter measures what it costs. A protected data member is reachable by every class that derives from yours — including classes written later, by people who have never read your invariant and are not obliged to maintain it — and that turns out to be a much wider exposure than it looks, because the one thing a protected field cannot do is stay consistent. The measurement is a base class whose invariant a three-line derived class destroys without writing anything the compiler could object to, next to the same base with the field made private and the operation made protected instead, where the identical derived class is simply refused.

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.