Module 6 — Concepts and Explanations¶
Read this with a compiler open. Every rule in it is one you can test in under a minute, and the two that surprise people — the declaration order that decides initialisation, and a diamond that produces no diagnostic at all — are the ones you should run rather than believe.
The running system, as of Module 5, holds one kind of item: a Book that is
comparable, printable and sortable. This module asks what happens when the library
acquires journals and DVDs, and answers with a hierarchy.
1. "Three kinds of item, and I am writing the same three things three times"¶
The catalogue now needs Book, Journal and Dvd. Each has an identifier, a
title, and a flag saying whether it is on the shelf. Written separately, that is
the identifier declared three times, the title declared three times, the flag
declared three times, and every future change to any of them made in three places.
// Three independent classes — the duplication is the whole complaint.
class Book { /* int id_; std::string title_; bool onShelf_; … */ };
class Journal{ /* int id_; std::string title_; bool onShelf_; … */ };
class Dvd { /* int id_; std::string title_; bool onShelf_; … */ };
Inheritance writes those three facts once, in a class that describes what all three are, and lets each of the three add what makes it different:
// examples/module-06/01-payroll-hierarchy/main.cpp
class Employee { // what every employee IS
public:
Employee(int id, std::string name, long long monthlyPaisa)
: id_(id), name_(std::move(name)), monthlyPaisa_(monthlyPaisa) {}
int id() const { return id_; }
const std::string& name() const { return name_; }
long long monthlyPaisa() const { return monthlyPaisa_; }
protected:
void setMonthlyPaisa(long long paisa) { monthlyPaisa_ = paisa; }
private:
int id_;
std::string name_;
long long monthlyPaisa_;
};
class SalariedEmployee : public Employee { // …and what THIS one adds
public:
SalariedEmployee(int id, std::string name, long long monthlyPaisa, int grade)
: Employee(id, std::move(name), monthlyPaisa), // the base part, first
grade_(grade) {}
int grade() const { return grade_; }
private:
int grade_;
};
SalariedEmployee never mentions id_, name_ or monthlyPaisa_, and it has
id() and name() anyway — that is what "inherits" means. The words to fix in
your memory are base class (the general one) and derived class (the
specialised one), and the relationship between them is spelled is-a.
The UML for that pair, in the notation the rest of this course uses:
┌─────────────────────────────┐
│ Employee │
├─────────────────────────────┤
│ - id_ : int │
│ - name_ : string │
│ - monthlyPaisa_: long long │
├─────────────────────────────┤
│ + id() : int │
│ + name() : string │
│ + monthlyPaisa(): long long │
│ # setMonthlyPaisa(long long)│
└──────────────▲──────────────┘
│ is-a (public inheritance)
┌──────────────┴──────────────┐
│ SalariedEmployee │
├─────────────────────────────┤
│ - grade_ : int │
├─────────────────────────────┤
│ + grade() : int │
│ + promote() : bool │
└─────────────────────────────┘
- is private, # is protected, + is public, and the hollow arrow means
"derives from". Every diagram in Modules 6–8 uses exactly these four conventions.
Predict before you run it. SalariedEmployee also declares a monthlyPay(),
and Employee has one too. Call monthlyPay() on a SalariedEmployee through a
reference to Employee — which function runs?
void payroll(const Employee& employee) {
std::cout << employee.monthlyPay(); // base version: the function is not virtual
}
The base one, every time, and the pay is wrong. That is not a bug in the language; it is the default. Module 7 is entirely about why it happens and how to change it. Module 5 already taught the symptom under another name — name hiding — and §6 of this page makes it precise.
2. "What exactly do I get, and what do I have to write myself?"¶
From examples/module-06/01-payroll-hierarchy/main.cpp, printed by the program:
The derived classes never wrote describe(), id() or name():
salaried.id() = 1
salaried.name() = Ayesha Khan
contractor.id() = 2
A derived class inherits, for free:
| Inherited | Not inherited |
|---|---|
| Public and protected member functions of the base | The base's constructors (unless you write using Base::Base; — §7) |
| The base's data members — one copy, inside every derived object | The base's destructor: each class destroys only its own part |
The base's static members — one per class, shared with derived objects |
Private members, in the sense that a derived class cannot touch them directly |
| The base's access levels, subject to the inheritance mode (§3) | Its copy/move operations as inherited functions: those are generated for the derived class, and they copy the base part by calling the base's |
Two sentences from that table are worth more than the rest. A derived object
contains a base subobject — it is not a copy of the base's code, it is the base's
data sitting inside the derived object, which is why construction order (§4) is
forced to be what it is. And private members stay private: SalariedEmployee
works through monthlyPaisa() and the protected setMonthlyPaisa(), never through
monthlyPaisa_. Private means private to the class, not to the family.
3. "Who can see what?" — access levels and inheritance modes¶
Two separate questions are always being confused here, so answer them separately. Inside the hierarchy, access is decided by the members' own access levels. Outside, access is decided by those levels as reduced by the inheritance mode.
Inside the hierarchy.
| Where the member is declared | Reachable from the class itself | From a derived class | From outside |
|---|---|---|---|
public: |
yes | yes | yes |
protected: |
yes | yes | no |
private: |
yes | no | no |
protected is a coupling decision, not a convenience. Anything protected is
part of the contract with every derived class, in every project that derives from
you. It is the narrowest thing that makes promote() possible in the program
above — and it is also the reason that changing a protected member can break code
you cannot see. This is the fragile base class problem, and it is why Module 4's
rule ("make it private") has exactly one acknowledged exception, marked in a
comment as a deliberate decision.
The inheritance mode. Run examples/module-06/02-inheritance-modes-and-access/
and the confusion dissolves, because the derived classes behave identically in all
three modes.
mode derived class reaches outside world sees is-a?
-----------------------------------------------------------------------------
public public + protected public stays public yes
protected public + protected nothing inherited public only to its own derived classes
private public + protected nothing inherited public no
The program's own output:
Every derived class reached protected members. That is not the mode
at work: `takeOut` and `balance` are reachable from a derived class in
all three modes, because access inside the hierarchy is decided by the
members' own access levels.
protected inheritance — the account is audited, and not an Account:
`savings.balance()` would not compile: the inherited member is
protected in AuditedAccount, so it is not public in AuditedSavings.
`audit(savings)` would not compile either.
private inheritance — a ledger entry is built using an Account:
`entry.deposit(1)` would not compile … `audit(entry)` would not
compile either — no is-a relationship exists.
And the conclusion the program prints about private inheritance, which is worth copying into your notes:
So what does
private Accountbuy? Almost nothing that a member would not buy more clearly:LedgerEntryneeds an amount, and a lonelong long amount_;member would say so without pretending to be an account.
Write public inheritance or do not write inheritance. protected and
private inheritance are legitimate — they mean "implemented using", they are
sometimes the right tool, and you should recognise them when you meet them — but in
this course's code the honest way to say "built using" is a member, which is §8.
A rule for class versus struct from Module 2 applies with a sharper edge here:
class D : Base — written without a mode — is private inheritance, and the
error it produces ('Base' is an inaccessible base of 'Derived') appears at the
call site, far from the omission.
4. "In what order do things happen?"¶
One trace answers every ordering question in this module.
examples/module-06/03-construction-and-destruction-order/main.cpp builds
Person → Employee → Manager, where each class owns a Tracer member that
announces itself when it is created or destroyed:
Constructing one Manager:
member Ayesha's second member constructed
member Ayesha's first member constructed
Person body (name = Ayesha)
member Ayesha's badge constructed
Employee body (id = 42)
member Ayesha's desk constructed
Manager body (reports = 7)
--- fully built: Ayesha, id 42, 7 reports
Manager body destroyed
member Ayesha's desk destroyed
Employee body destroyed
member Ayesha's badge destroyed
Person body destroyed
member Ayesha's first member destroyed
member Ayesha's second member destroyed
Three rules, and the trace is nothing more than them:
- Bases before derived, all the way up —
Person, thenEmployee, thenManager. - Within one class, members in declaration order, before that class's own
constructor body.
second_is declared beforefirst_, so it is constructed first — and the initialiser list in the source is written in declaration order to keep the compiler quiet, not because that is what causes the order. - Destruction is the exact reverse. Read the second list backwards and you have the first.
The rule to carry into Module 7 is the second half of rule 1: at the moment
Person's constructor body runs, the Employee and Manager members do not exist
yet. Anything that depends on the derived part — including a virtual call — cannot
work there, and the language will not pretend otherwise.
5. "The base has no default constructor. Now what?"¶
A derived constructor must call a base constructor. If it does not name one, the compiler calls the base's default constructor — and if there is not one, the build fails. The only place to name it is the member initialiser list:
class Base {
public:
explicit Base(int id) : id_(id) {}
private:
int id_;
};
class Derived : public Base {
public:
Derived() : Base(7) {} // the only way to build the base part
};
Below is the real message from omitting it, captured from a scratch file compiled with this course's command. Note that the compiler is not confused — it lists every constructor it tried, which is the useful part of the output:
m6a.cpp: In constructor 'Derived::Derived()':
m6a.cpp:15:15: error: no matching function for call to 'Base::Base()'
| ^
m6a.cpp:3:14: note: candidate: 'Base::Base(int)'
m6a.cpp:3:14: note: candidate expects 1 argument, 0 provided
Inside the initialiser list, members may be written in any order and initialised in declaration order; the compiler warns when the two disagree:
error: 'Person::first_' will be initialized after [-Werror=reorder]
error: 'Tracer Person::second_' [-Werror=reorder]
error: when initialized here
That warning exists because writing them the other way round is a real mistake. It
applies to members only: a base is always initialised first, and no line of the
initialiser list can change it. With -Wall -Wextra it arrives as a warning; the
course compiles examples with -Werror, so it is a build failure here.
6. "Both classes have a function called print. Which one runs?"¶
The derived declaration hides every base function of that name, whatever their
signatures. examples/module-06/04-hiding-ambiguity-and-the-diamond/main.cpp shows
the three overloads disappearing at once:
1. Name hiding — the derived declaration hides all three base overloads:
ColourPrinter::print(string, bool) essay [colour]
These three would NOT compile on a ColourPrinter:
cp.print(3); error: no matching function for call to
'ColourPrinter::print(int)'
cp.print("essay"); (same error — the string overload is hidden too)
cp.print(); (same error — so is the no-argument one)
Two ways out, and the difference matters:
class ColourPrinter : public Printer {
public:
using Printer::print; // 1. bring all three base overloads back
void print(const std::string& label, bool colour) const;
};
or qualify the call at the point of use — cp.Printer::print(3); — which fixes one
call site rather than the design. Hiding is not the same as overriding; that
word is reserved for a virtual function replaced in a derived class, which is
Module 7, and the reason override exists as a keyword.
7. "Two bases, and a base that is itself shared" — multiple inheritance¶
class Cyborg : public Person, public Machine is legal. If both bases provide
id(), an unqualified call is ambiguous, and the compiler says so at the call
site:
error: request for member 'id' is ambiguous
26 | return cyborg.id() + more.value();
| ^~
note: candidates are: 'int Machine::id() const'
note: 'int Person::id() const'
That error is a gift: the compiler has pointed at a design question. Now make both bases derive from a common ancestor:
This is the diamond problem, and examples/module-06/04-…/main.cpp shows what
it actually costs:
3. The diamond — and this one produces no diagnostic at all:
ta.Student::name() = Ayesha (student record)
ta.Tutor::name() = Ayesha (staff record)
after ta.Student::rename("Ayesha Khan"):
ta.Student::name() = Ayesha Khan
ta.Tutor::name() = Ayesha (staff record) <- unchanged: a different Person
TeachingAssistant has two Person subobjects. Two names, two identifiers, two
of everything Person holds. Renaming through one leaves the other alone, and
nothing warns you, because two subobjects is what the code asked for. This is how
you recognise the diamond in the wild: one object, two answers to the same question.
The standard fix is virtual inheritance:
class Student : public virtual Person { };
class Tutor : public virtual Person { };
class TeachingAssistant : public Student, public Tutor { }; // one shared Person
Virtual inheritance keeps a single shared base subobject, and it has its own costs — the most-derived class becomes responsible for initialising the virtual base, and object layout becomes implementation-defined. It is the right tool for interface-only diamonds (a source of pure virtual functions, Module 8) and a poor one for shared state. When state is genuinely shared between two roles of one entity, an association — a pointer, Module 9 — says it without the layout surgery.
One more way to stop a mistake before it happens: class MoreLast : public Last
where Last is marked final gives
final belongs on a class whose specialisation would be unsafe or meaningless, and
on a function whose implementation must not be replaced (Module 7).
8. "Should this be inheritance at all?"¶
The failing case is the one every beginner writes. A fixed-capacity stack of integers, modelled by inheriting from a general list type, because the storage and the size are already written:
class IntList {
public:
void pushBack(int value);
bool insertAt(int index, int value);
void clear();
int size() const;
bool at(int index, int& out) const;
bool removeAt(int index);
};
class BrokenStack : public IntList { // ← "a stack IS-A list"? No.
public:
bool push(int value) { if (isFull()) return false; pushBack(value); return true; }
bool isFull() const { return size() == capacity_; }
…
};
The capacity rule is enforced in push, and bypassed by anything else the base
makes public. The program's output records the consequences:
Now three lines that any caller may write, because the base
made them public. Each one compiles:
stack.pushBack(99) <- no capacity check
stack.insertAt(0, 42) <- inserted at the BOTTOM of a stack
stack [42 1 2 3 99] size 5
isFull() is now false, and size() is 5 of a capacity of 3 — the rule is gone.
The composed version of the same capability — one private IntList member — is
smaller and cannot be attacked at all:
And the same attacks on the composed version, which would not compile:
stack.pushBack(99) error: 'class FixedStack' has no member
named 'pushBack'
stack.insertAt(0, 42) error: no member named 'insertAt'
stack.clear() error: no member named 'clear'
A capability that cannot be misspelled needs no documentation.
This is the test, stated as a sentence rather than a preference:
A
FixedStackis anIntList? No — you cannot insert at an arbitrary index, or clear a stack, and still be a stack. AFixedStackis implemented using anIntList? Yes — and that is composition.
The general form of the test is Liskov's substitution principle: anything true of
the base must stay true of every derived class. A stack that can be given an
arbitrary insertAt breaks that, because a function written against IntList — say
"insert a header at index 0" — is broken the moment a BrokenStack is passed to it.
Three questions, then, before you write : public Base:
- Is every operation of the base true of the derived type? If any one of them is not, do not inherit — or do not inherit that operation.
- Does the derived type genuinely is the base? "A
Manageris anEmployee" — yes. "ACaris anEngine" — no, however much code it saves. - Are you inheriting an interface, or an implementation? Inheriting an implementation binds you to the base's internals forever; that is what composition is for, and Module 9 works through the three has-a forms.
Checkpoint¶
Answer these before moving on. Each one is testable — write the program rather than reasoning about it.
Derivedderives fromBase, which has a protected memberp_and a private memberq_. Which of the two canDerived's member functions name directly, and which can they reach only through a base function?- In
class Manager : public Employee, write the two lines of the initialiser list that build the base part before the members, and say what the compiler does if you write the members first instead. - A base class declares
void print(int); a derived class declaresvoid print(std::string). A caller writesd.print(3). What happens, why, and what is the smallest fix? - Two bases both derive from
Person. How manyname_strings doesTeachingAssistantcontain, and which one changes when you callta.Student::rename("…")? - You are asked for a
Dequethat reuses aVector's storage. Write the sentence that decides whether it is inheritance or composition, and name one operation that would break the inheritance design.
Next: Worked examples — six programs, including the three-line attack on an inherited invariant.