Skip to content
VLSI Mentor

C++ Part I · Module 16 · Inheritance

Base and Derived Classes

The previous chapter argued about what a derivation asserts and asked you to take one mechanical fact on trust: that the base part sits inside the derived object rather than alongside it. That fact is worth seeing properly, because four later chapters depend on it. Construction order makes sense only once you know the base part is a region of the same object that has to be built before the rest of it. The conversion to a base reference being free — no copy, no adjustment, no bookkeeping — is a consequence of where the base part sits. And the by-value surprise that Module 17 returns to is a consequence of the same geometry read the other way. So this chapter declares a derivation properly and measures the result: the sizes, the position of every member, and the address relationship that makes the whole thing work, with one picture of a single object holding two parts. Then the half of the chapter that sets up the two chapters after it. A derived class can reach most of what its base has, but not all of it, and the boundary is not where people expect: the member a derived class cannot touch is private, the member it can is protected, and the difference between those two words turns out to be a design decision about who you are willing to let break your invariant rather than a detail about syntax.

Intermediate17 min readC++Part IFundamentalsInheritance

Part I · Module 16 · Chapter 16.2 · Base and Derived Classes

1. Declaring a Derivation

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

    struct WriteTransaction : Transaction {
        unsigned mask = 0xF;
    };

: Transaction is the whole syntax. Three things about it worth stating before the measurements.

The access specifier is implicit here. struct D : B means struct D : public B; for a class the default is private. Chapter 16.3 §9 explains why that asymmetry exists and why you should always write public explicitly — this chapter writes struct for brevity in listings and means public throughout.

The base must be a complete type. A forward declaration is not enough, because the compiler has to know how big the base part is to lay out the derived object.

And a class may have several bases. Multiple inheritance is legal, rarely the right answer outside interface classes, and not this module's subject.

2. What the Object Becomes

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

Eight bytes of base plus four of mask. Chapter 9.8 §5 established that sizeof is not always the sum of the members' sizes — padding can make it larger — and here the arithmetic happens to be exact because everything is 4-byte aligned.

The important measurement is not the size but the position:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. member offsets, by pointer arithmetic (no offsetof):
     addr @ 0   data @ 4   mask @ 8

The base's members come first, at the offsets they would have in a standalone Transaction, and the derived member follows. Which gives the address relationship:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. the base subobject shares the derived object's address:
     &w == base subobject ? 1

The same address. A WriteTransaction and its Transaction part begin at the same byte.

3. One Object, Two Parts

A WriteTransaction object of 12 bytes contains a Transaction subobject of 8 bytes at offset 0, so the derived object and its base part share one address.WriteTransaction12 bytes totalTransaction part8 bytes at offset 0One objectone address, twopartscontainsmeasured12
Figure 1 — why a derived object and its base part share one address. A WriteTransaction of 12 bytes contains an 8-byte Transaction subobject at offset 0, so a base pointer and a derived pointer hold the same value while describing different amounts of storage. That single fact is what makes a base reference usable, and what makes copying by value lose the derived half.

The word to resist is "two objects". There is one object of 12 bytes. The first 8 of those bytes are simultaneously the WriteTransaction's addr and data and a complete, usable Transaction — not a copy of one, not a view of one, but the thing itself, occupying storage that the derived object also occupies.

That is why §4's conversion costs nothing, and it is the picture to hold for Chapter 16.5's construction order: building a WriteTransaction means building the Transaction part first, in place, and then the rest.

4. The Conversion Is Free

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void observes(const Transaction& t) { std::printf("     observed addr=0x%X\n", t.addr); }
    observes(w);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. a derived object is usable where a base is expected:
     observed addr=0x40

And from Chapter 16.1 §5:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
     &r == &w ? 1   (no copy; same object)

A Transaction& bound to a WriteTransaction holds the same address. The conversion is not a conversion in any runtime sense — no copy, no arithmetic, no stored offset. The compiler changes the type of the expression and the pointer value is unchanged, which is a compile-time operation costing zero instructions.

This is worth knowing precisely because it is the thing people assume is expensive. Chapter 15.1 §4 and Chapter 14.2 §5 both measured abstractions costing nothing; this is another. Passing a derived object to a base-reference parameter is as cheap as passing it to its own type.

(That the base is at offset 0 is the common case rather than a guarantee. With multiple inheritance a second base cannot also be at offset 0, so the conversion to it does adjust the pointer — still a compile-time constant, still trivial, but not a no-op. Single public inheritance, which is what this module is about, gives the offset-0 case.)

5. Taken by Value, Only the Base Part Goes

The symmetric fact, from Chapter 16.1 §5:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. by value -- only the base part is copied:
     Transaction copy = w;  sizeof(copy)=8  addr=0x40

The §3 picture explains it exactly. A Transaction object is 8 bytes; there is no field in one that could hold a mask. Copy-initialising a Transaction from a WriteTransaction copies the base part and stops, because that is all a Transaction has room for.

So: by reference or pointer, the whole object travels; by value, a base object is constructed from the base part. Both are correct; the second surprises people, and Chapter 17.6 is the chapter on why it becomes dangerous once virtual functions exist. For now the rule from Chapter 6.5 suffices: take parameters of class type by const&.

6. A Base Pointer Does Not Index a Derived Array

One consequence of §3's picture is worth measuring because it is silent, and because it is the only way the free conversion of §4 can hurt you.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    WriteTransaction a[3];
    a[0].addr = 0x10; a[1].addr = 0x20; a[2].addr = 0x30;

    Transaction* p = a;                   // legal: array decay, then derived-to-base
    for (int i = 0; i < 3; ++i) std::printf("     p[%d].addr = 0x%X\n", i, p[i].addr);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. through the correct type:
     a[0].addr = 0x10
     a[1].addr = 0x20
     a[2].addr = 0x30
B. through a BASE pointer -- strides by sizeof(Transaction)=8, not 12:
     p[0].addr = 0x10
     p[1].addr = 0xF
     p[2].addr = 0x0
C. the byte offsets make it obvious:
     &a[1] - &a[0] = 12   |  &p[1] - (char*)&a[0] = 8
     &a[2] - &a[0] = 24   |  &p[2] - (char*)&a[0] = 16

p[1].addr read 0xF, which is not an address at all — it is a[0].mask. The array's elements are 12 bytes apart; p[i] steps 8 bytes at a time, because pointer arithmetic strides by the pointer's type. After the first element, p[i] names bytes that straddle two array elements.

Chapter 8.4 §2 measured that pointer arithmetic scales by the pointed-to type, and Chapter 16.1 §4's conversion is what lets the two types disagree here. Both facts are correct and their combination is not.

The reason this one deserves a section is the diagnostics:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-Wall -Wextra -Wpedantic : 0 warnings
-Weverything             : 11 warnings -- and I read them: C++98-compat,
                           old-style-cast, include paths. None about the stride.
AddressSanitizer         : nothing

ASan is silent for a principled reason: the reads are not out of bounds. p[2] lands at byte 16 of a 36-byte array — inside the storage, just not at the start of an element. It is the wrong bytes, correctly addressed, which is exactly the category no bounds checker can represent.

So the rule is narrow and absolute: never index through a base pointer into an array of derived objects. If you need a sequence of objects handled polymorphically, hold pointers — std::vector<Transaction*> or, per Chapter 14.1 §9, std::vector<std::unique_ptr<Transaction>> — where every element is the same size whatever it points at.

7. Which Base Members a Derived Class Can Touch

Three access levels in the base, and a derived member function trying each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Base {
    public:    int pub  = 1;
    protected: int prot = 2;
    private:   int priv = 3;
    };
    struct Derived : Base {
        int sumAll() { return pub + prot + priv; }
    };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: 'priv' is a private member of 'Base'
note: declared private here

pub and prot are fine; priv is not. A derived class is not a friend of its base — it gets the public and protected interface and nothing more. The base's private members are in the object (they are part of the 8 bytes) but not reachable from derived code.

And protected is not public. From outside the hierarchy:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: 'prot' is a protected member of 'Base'
note: declared protected here

So the access table for a public base:

In the basePublic codeDerived member functions
publicyesyes
protectednoyes
privatenono

protected is exactly the row that exists for inheritance — accessible to derived classes and to nobody else. Chapter 11.4 introduced the specifiers; what protected is actually for, and why a protected data member is usually a mistake, is Chapter 16.4's subject, and §7's refusal is the measurement that sets the question up.

8. Reaching the Base Part Explicitly

Three ways, in order of how often you want them.

Just use the member. w.addr works; the name is found in the base and the compiler knows where the base part is.

Qualify it when a name is ambiguous or hidden:

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

w.Transaction::addr names the base's member explicitly. This matters when the derived class declares something with the same name, which Chapter 16.6 measures — and inside a derived member function the form is Transaction::addr.

And convert to the base explicitly when you need the base's behaviour:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static_cast<Transaction&>(w)          // a Transaction& referring to the base part

Needing this third form is usually a signal. Chapter 16.6 §7 shows the one legitimate use — calling a base version of a function the derived class has redeclared — and Chapter 16.8 §4 treats a scattering of such casts as a symptom.

9. What Deriving Costs the Layout

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)

Data members in both the base and the derived part cost you standard layout. The middle row is the exact boundary — deriving is not what does it; adding a data member to a derived class whose base already has one is.

Three consequences, all of which matter for work at a C boundary:

offsetof becomes conditionally-supported. I met this as a diagnostic rather than a prediction — my first attempt at measuring §2's offsets used offsetof and drew three -Winvalid-offsetof warnings, which is how the fact surfaced. The offsets in §2 came from pointer arithmetic instead.

Byte-wise copying across a boundary stops being guaranteed, which Chapter 9.8 §9 measured for a plain struct's padding — same concern, inheritance clause.

And a type meant to mirror a C struct no longer reliably does. For DPI and firmware work this is the practical reason to keep wire-format and register-map types as plain structs and put the behaviour somewhere else.

10. What to Actually Do

Write public explicitly — class D : public B, never relying on the default.

Think of one object with two parts, not two linked objects.

Pass by const& or pointer when the parameter is a base type.

Expect the base-reference conversion to be free — measured, the same address.

Use the base's public and protected members; do not expect its private ones.

Qualify with Base:: only when a name is hidden, and treat frequent static_cast to a base as a smell.

And keep wire-format and register-map types as plain structs if anything depends on their memory image.

11. Mistakes Worth Making Once

Picturing the base as a separate, linked object. It predicts an indirection, a copy, and a swappable base — all wrong.

Relying on class D : B's default. That is private inheritance, which means something quite different.

Expecting a derived class to reach the base's private members. Measured: 'priv' is a private member of 'Base'.

Thinking protected is nearly public. Measured: refused from outside the hierarchy.

Assuming the base-reference conversion copies something. Measured: the same address.

Taking a base parameter by value. Only the base part arrives — 8 bytes of a 12-byte object.

Using offsetof on a derived class with data in both parts. Not standard-layout; it warns.

memcpying such a type across a C boundary. The layout is no longer guaranteed.

Assuming the base is always at offset 0. True for single inheritance, not in general.

12. Interview Relevance

"What does a derived object contain?" A complete base subobject plus the derived members — one object, measured 8 bytes becoming 12, with the base part at offset 0.

"What does converting a derived reference to a base reference cost?" Nothing. Measured as the same address; it is a compile-time type change. The strong answer notes this holds for single inheritance and that a second base must be adjusted.

"Can a derived class access its base's private members?" No. It gets public and protected only — a derived class is not a friend of its base.

"So what is protected for?" Exactly this row: derived classes yes, everyone else no.

"What happens if you pass a derived object by value as a base?" A base object is constructed from the base part; the derived data has nowhere to go.

"Does inheritance change the layout guarantees?" Yes — data in both parts makes the type non-standard-layout, so offsetof and byte-wise copying stop being guaranteed. Rarely known, and directly relevant to DPI work.

13. What You Should Be Able to Say Now

  1. : Base is the whole syntax, and struct defaults to public while class defaults to private — so write it explicitly.
  2. The base must be a complete type, because its size determines the derived layout.
  3. Measured: sizeof 8 → 12, with addr @ 0, data @ 4, mask @ 8.
  4. Measured: the base subobject shares the derived object's address.
  5. One object with two parts, not two linked objects.
  6. Measured: binding a const Transaction& to a WriteTransaction gave the same address — the conversion is free.
  7. That holds for single inheritance; a second base cannot also be at offset 0.
  8. Measured: by value, only the base part arrives — sizeof(copy)=8.
  9. Measured: a derived class can touch public and protected base members; priv is a private member of 'Base'`.
  10. Measured: protected is refused from outside the hierarchy.
  11. A derived class is not a friend of its base.
  12. Private base members are present in your object's bytes and not nameable from your code — presence and access are different questions.
  13. w.Transaction::addr names a base member explicitly, which matters when a name is hidden.
  14. Measured: data in both parts gives is_standard_layout = 0; adding no data members keeps it 1.
  15. The standard-layout fact surfaced as three -Winvalid-offsetof warnings — I re-measured the offsets with pointer arithmetic.

14. Exercises

Exercise 1 — declare it. Write a Packet with two unsigned members and a TaggedPacket adding one, with the access specifier written explicitly.

Exercise 2 — predict the layout. Give both sizeof values and all three member offsets, then say what the address relationship is.

Exercise 3 — the picture. Explain in two sentences why the base-reference conversion costs nothing, referring to the layout.

Exercise 4 — by value. Say what Packet p = tagged; produces and why the tag has nowhere to go.

Exercise 5 — the access table. Fill in the three rows for public code and derived member functions, and give the diagnostic for the refused cases.

Exercise 6 — presence versus access. A base has a private int. Say whether it occupies space in a derived object and whether derived code can name it.

Exercise 7 — qualified access. Say when d.Base::value is necessary, and name the chapter that measures the situation.

Exercise 8 — layout cost. Classify three types as standard-layout or not, and explain what breaks for the one that is not.

Exercise 9 — the two-object model. Give three concrete predictions the "two linked objects" model makes, and say why each is wrong.

15. Summary

  • : Base declares a derivation; write public explicitly, and the base must be complete.
  • Measured: sizeof 8 → 12, offsets 0, 4, 8, and the base subobject at the derived object's own address.
  • One object with two parts — the model that correctly predicts no indirection, no copy, and no swappable base.
  • Measured: the base-reference conversion is free, the same address, a compile-time type change.
  • Measured: by value only the base part travels — sizeof(copy)=8.
  • Measured: derived code reaches public and protected, not private — a derived class is not a friend of its base.
  • Private base members are present but not nameable — presence and access are separate questions.
  • Measured: data in both parts costs standard layout, which surfaced as -Winvalid-offsetof warnings and matters at any C boundary.

The mechanism is now visible, and that makes the central question of the module answerable rather than abstract. Chapter 16.1 stated that public inheritance asserts substitutability, but stating a claim is not the same as having a way to check one — and the check is what decides every design in this module. The next chapter turns the claim into a test you can apply before writing : Base, and then applies it to the hierarchy that every textbook uses as the cautionary example, measuring what actually goes wrong. The measurement is more interesting than the usual telling: run twice, once with plain member functions and once with virtual ones, the same two classes fail in two different ways — one run breaks the derived class's own invariant while keeping the base's contract, and the other keeps the invariant and breaks the contract. Neither run is correct, which is the point: the problem was in the claim, and no mechanism fixes a false claim.

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.