Skip to content
VLSI Mentor

C++ Part I · Module 16 · Inheritance

What Inheritance Models

Five modules have been about making one type behave well. This one is about a relationship between two, and it opens with a warning rather than a mechanism, because inheritance is the feature in C++ most often used for the wrong reason. The syntax is three characters and it is easy to reach for the moment you notice that one class already has members another one needs — which is exactly the motivation that produces hierarchies nobody can later change. What deriving publicly actually asserts is much stronger than code sharing: it says every object of the derived type is usable anywhere an object of the base type is expected, by code that has never heard of the derived type and will never be recompiled when you add another one. That is a promise about behaviour made to strangers, and the reason most inheritance mistakes are hard to fix later is that the promise was never true. This chapter measures what the derived object actually becomes, measures that it is genuinely accepted wherever the base is, and then measures the cost of the reuse motivation on a concrete case — a stack built by inheriting from a vector, which turns out to permit clearing itself, inserting at the bottom, and overwriting an element in the middle, while being exactly the same size as the version that contains a vector instead. Same bytes, same speed, one of them with an interface it cannot defend.

Intermediate17 min readC++Part IFundamentalsInheritance

Part I · Module 16 · Chapter 16.1 · What Inheritance Models

1. Two Reasons People Derive

One of them is a reason and the other is a symptom.

"A WriteTransaction is a kind of Transaction." A claim about the world the program models. Code that handles transactions should handle write transactions, because a write transaction is one.

"Transaction already has addr and data, and I need those." A claim about your convenience. Nothing about the relationship between the types; just an observation that some members already exist.

The second is where almost all inheritance damage comes from, and it is persuasive precisely because it is true — the members really are there, and deriving really does give you them. §6 and §7 are about why that is not enough.

2. What the Syntax Does

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Transaction      { unsigned addr = 0; unsigned data = 0; };
    struct WriteTransaction : Transaction { unsigned mask = 0xF; };

The : Transaction makes Transaction a base class of WriteTransaction, and three things follow mechanically:

The derived object contains a base subobject. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. sizes:
     sizeof(Transaction)      = 8
     sizeof(WriteTransaction) = 12

Eight bytes of Transaction plus four of mask. The base part is not a reference to something else — it is inside the object, and Chapter 16.2 measures its position.

The base's accessible members are reachable through the derived object.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
E. and the base's members are reachable directly:
     w.addr=0x40  w.Transaction::addr=0x40

And the derived type converts to the base type — which is §3's subject and the one that matters.

3. What the Syntax Asserts

Here is the claim public inheritance makes, and it is worth reading twice because it is stronger than it looks:

Every WriteTransaction is usable wherever a Transaction is expected — by code that does not know WriteTransaction exists.

Not "shares members with". Not "is implemented using". Usable in place of, by strangers.

That is a promise with two uncomfortable properties. It is made to code you have not seen, including code written after yours. And it is about behaviour, not just about which members compile — a function that accepts a Transaction& was written against what Transaction promises, and it is entitled to those promises from every object it receives.

The name for this is the substitutability requirement, and Chapter 16.3 applies it as a test and measures a hierarchy failing it.

4. The Assertion Is Real, Not Notional

The compiler acts on §3's claim immediately. A function taking a base reference:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void observes(const Transaction& t) { std::printf("     observed addr=0x%X\n", t.addr); }

    WriteTransaction w;
    w.addr = 0x40;
    observes(w);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. a derived object is usable where a base is expected:
     observed addr=0x40

No cast, no conversion function, no copy. The WriteTransaction was passed to a function expecting a Transaction and the function read the base part directly — because the base part is genuinely there, at a known position, and Chapter 16.2 measures that it shares the derived object's address.

This is the whole of what public inheritance buys, and it is a lot: one function, any number of derived types, no recompilation when you add another. It is also exactly what makes a false claim expensive, because every such function is a place where the promise is relied on.

5. The Claim's Honest Limit

§4's demonstration used a reference, and that detail is load-bearing. Measured against the by-value form:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void byRef  (const Transaction& t);
    static void byValue(Transaction t);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the WriteTransaction: sizeof=12 mask=0x3
A. through a reference -- the whole object is still there:
     byRef   sizeof(param-type)=8 addr=0x40
     &r == &w ? 1   (no copy; same object)
B. by value -- only the base part is copied:
     byValue sizeof(param-type)=8 addr=0x40
     Transaction copy = w;  sizeof(copy)=8  addr=0x40

Through a reference, nothing was copied — &r == &w is 1, the same object, all twelve bytes of it, merely viewed as its base part.

By value, only the base part came across. Transaction copy = w; produced an 8-byte object, and mask is not merely unset — there is no field in a Transaction that could hold it.

So §3's claim is precise about its own scope: a derived object is usable wherever a base object is expected, and when the base is taken by reference or by pointer the derived object goes along whole. Taken by value, what arrives is a Transaction built from the base part, which is a different object. That is correct and occasionally what you want; it is also a well-known source of surprise, and Chapter 17.6 is the chapter on it, because the surprise only becomes dangerous once virtual functions make the discarded part behaviourally significant.

The practical consequence is the one Chapter 6.5 already argued on cost grounds, now with a second reason: take polymorphic parameters by const& or by pointer, never by value.

6. The Reuse Motivation

Now the common case. You need a stack of integers. std::vector<int> already has push_back, pop_back, back, size and empty. Deriving is three characters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct StackInherited : std::vector<int> {
        void push(int v) { push_back(v); }
        int  pop()       { int v = back(); pop_back(); return v; }
    };

It works. It is short. And applying §3's test to it gives an immediate answer: is every StackInherited usable wherever a std::vector<int> is expected? The honest answer is that nobody asked that question, because nobody wanted it to be true — what was wanted was push_back.

The alternative holds the vector as a member and forwards:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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_;
    };

More lines. §7 is what those lines buy.

7. What Reuse Costs, Measured

Both versions do the job:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. both support the intended interface:
     inherited: size=2 pop=2
     composed : size=2 pop=2

The inherited one also does these:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. but 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=0

A write into the middle, an insertion at the bottom, and a wholesale clear — all compiled, all mutating a thing whose entire purpose is last-in-first-out access. There is nothing to stop any of them, because §3's claim was made and the compiler believed it: a StackInherited is a std::vector<int>, so everything a vector can do, it can do.

On the composed version the same three are refused:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. on the COMPOSED stack, the same three are REFUSED:
     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'

And the measurement that removes the last argument for inheriting:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. sizes:
     sizeof(StackInherited) = 24
     sizeof(StackComposed)  = 24

Twenty-four bytes either way. Composition is not a layer, not an indirection, and not a cost — the member is the vector, at the same size, and forwarding functions inline to nothing (Chapter 15.1 §4 measured the same for operators). So the reuse motivation buys a few lines of typing and costs an interface you cannot defend.

8. The Question to Ask Instead

Before writing : Base, the question is not which members do I need. It is:

Would I be comfortable if a function that knows only about Base, written by someone else, received my object and relied on everything Base promises?

If yes, derive. If no, or if you have to think about edge cases, you wanted a member.

Two useful reformulations:

Does the derived type promise everything the base promises, and more? Inheritance may add. It must never remove or weaken — Chapter 16.8 §5 measures a derived class narrowing a promise.

And would I put it in a container of base objects? std::vector<Transaction*> holding write transactions is the shape public inheritance exists for. A vector of std::vector<int>* holding stacks is a question nobody wants to answer.

9. One Measured Cost Worth Knowing

Inheritance is not quite free, and the cost is not where people look for it. It is in layout guarantees:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
E. standard layout -- and what inheritance costs it:
     is_standard_layout<Transaction>      = 1
     is_standard_layout<DerivedNoData>    = 1   (base has data, derived adds NONE)
     is_standard_layout<WriteTransaction> = 0   (data in BOTH -> not standard layout)

A class with data members in both the base and the derived part is not standard-layout. The middle row is the precise boundary: deriving alone does not cost it — a derived class that adds no data members stays standard-layout. Adding even one does not.

That matters for a specific and, for this audience, common set of tasks: anything relying on a predictable memory image. offsetof becomes conditionally-supported, memcpy of a whole object across a boundary stops being guaranteed, and a C struct the type is supposed to mirror may no longer match. Chapter 9.8 §9 measured what padding breaks for a plain struct — byte-wise comparison and any assumption about the memory image; this is the inheritance clause of the same concern.

(I met this as a diagnostic rather than a prediction: my first attempt at measuring member offsets used offsetof and drew three -Winvalid-offsetof warnings, which is how the finding turned up. I re-measured the offsets with pointer arithmetic.)

10. What This Module Holds Back

Everything in this chapter is inheritance without polymorphism, and that is deliberate.

virtual functions, dynamic dispatch, abstract base classes, override, and the two classic hierarchy bugs — a missing virtual destructor and a sliced object — are Module 17. Not because they are advanced, but because of an ordering claim worth stating:

Almost every inheritance mistake is made before any virtual function is written.

The stack in §7 has no virtual functions and is already a bad design. The hierarchy Chapter 16.3 measures failing substitutability fails it identically with and without virtual — the chapter measures both. So the decisions this module is about are decisions about relationships, and they are settled before dispatch enters the picture.

11. What to Actually Do

Ask §8's question before writing : Base — would a stranger relying on the base's promises be right to?

Derive for a relationship, never for members you wanted.

Default to composition, and measure if you think it costs something — 24 against 24.

Treat the base's whole public interface as part of your type's interface, because it is.

Check is_standard_layout if the type crosses a C or memory-image boundary.

And settle the relationship before thinking about virtual — the decision does not depend on it.

12. Mistakes Worth Making Once

Deriving because the members were already there. Measured: the stack gained clear(), insert() and subscript.

Assuming composition costs something. Measured: 24 bytes against 24, with forwards that inline away.

Thinking of the base part as a reference to another object. It is inside yours — 8 bytes of a 12-byte object.

Reading : Base as "shares code with". It asserts usable-in-place-of, to strangers.

Deciding substitutability on which members compile. The claim is about behaviour.

Using offsetof on a derived class with data in both parts. It is not standard-layout — measured, and it warns.

Assuming a derived class is non-standard-layout merely for deriving. Measured: adding no data members keeps it.

Postponing the design question until you need virtual. The question is already answerable.

13. Interview Relevance

"What does public inheritance mean?" That every derived object is usable wherever a base object is expected, by code that does not know the derived type. The strong answer leads with the claim, not the syntax.

"Why not inherit from std::vector to make a stack?" Because you inherit its entire interface — measured, clear(), insert() and a write into the middle all compile — and composition is the same size.

"Does composition cost more than inheritance?" No. Measured 24 bytes against 24, with forwarding functions inlined away.

"How do you decide between them?" Ask whether a stranger relying on the base's promises would be right to. If not, make it a member.

"Is the base object a separate object?" No — it is a subobject inside the derived object, sharing its address. sizeof went 8 to 12.

"Does inheritance affect layout guarantees?" Yes — data members in both the base and derived parts make the type non-standard-layout, which matters for offsetof and C interop. Few candidates know this.

14. What You Should Be Able to Say Now

  1. There are two motivations for deriving, and only the relationship is a reason.
  2. Public inheritance asserts that every derived object is usable wherever a base object is expected — by code that has never heard of the derived type.
  3. The claim is about behaviour, not about which members compile.
  4. Measured: sizeof went 8 → 12 — the base part is inside the derived object.
  5. Measured: a WriteTransaction was passed to a function taking const Transaction& with no cast and read correctly.
  6. That is the feature — one function, many derived types, no recompilation.
  7. And that is why a false claim is expensive: every such function depends on it, and you cannot enumerate them.
  8. Measured: a stack built by inheriting from std::vector<int> permitted s[1] = 999, insert(begin(), 7) and clear().
  9. Measured: the composed version refused all three with clear diagnostics.
  10. Measured: both are 24 bytes — composition costs nothing.
  11. So the reuse motivation buys typing and costs an interface you cannot withdraw.
  12. The question to ask: would a stranger relying on the base's promises be right to?
  13. Inheritance may add to a promise, never remove or weaken one.
  14. Measured: data in both parts makes a class non-standard-layout; adding no data members keeps it.
  15. Almost every inheritance mistake is made before any virtual function is written — which is why Module 17 is held back.

15. Exercises

Exercise 1 — two motivations. For three pairs of your own choosing, say which motivation a derivation would be serving.

Exercise 2 — state the claim. Write out what struct Fast : ReferenceModel asserts, in one sentence, without using the word "inherit".

Exercise 3 — predict the sizes. A Packet with two unsigned members, and a TaggedPacket adding one unsigned. Give both sizeof values and say where the base part lives.

Exercise 4 — the exposure. List five std::vector operations a stack should not have, and say what stops them in each design.

Exercise 5 — the cost. A colleague says composition adds an indirection. Give the measurement that answers it.

Exercise 6 — apply the question. For each, derive or contain: a Square and a Rectangle; a Car and an Engine; a WriteTransaction and a Transaction; a Logger and a std::ofstream; a CachedModel and a ReferenceModel.

Exercise 7 — standard layout. Say which of three classes is standard-layout: a plain struct; a derived class adding no data; a derived class adding one int. Then say why it matters for DPI work.

Exercise 8 — the ordering claim. Explain why the stack in §7 is already a bad design despite having no virtual functions.

16. Summary

  • Inheritance is a modeling claim, not a code-sharing trick. Public inheritance asserts every derived object is usable wherever a base object is expected, by code that has never heard of it.
  • The claim is about behaviour, and it is made to strangers you cannot enumerate — which is what makes a false one expensive.
  • Measured: sizeof 8 → 12, the base subobject inside the derived object, and a derived object accepted by a const Transaction& parameter with no cast.
  • Measured: a stack that inherited from std::vector<int> permitted a write into the middle, an insert at the bottom, and clear(); the composed version refused all three.
  • Measured: both are 24 bytes. Composition costs nothing, so reuse-by-inheritance buys typing and costs an interface you cannot withdraw.
  • Ask whether a stranger relying on the base's promises would be right to. If not, make it a member.
  • Measured: data in both parts makes a class non-standard-layout — the inheritance clause of Chapter 9.8's rules.
  • The design question is settled before virtual exists, which is why Module 17 is held back.

The claim is stated and the motivation is dealt with. What has not been shown is what the derived object actually is — §4 asserted that the base part sits inside it and shares its address, and that is worth seeing rather than taking on trust, because several later chapters depend on it. The next chapter declares a derivation properly, measures where each member lands, and shows the one picture this module needs: a single object with two parts, one address, and a base subobject whose position explains why passing a derived object to a base-reference parameter costs nothing at all. It also measures which base members a derived class can actually touch, which turns out not to be all of them — and the member it cannot reach is the one that sets up the access question the two chapters after it are about.

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.