Module 6 — Worked Examples¶
Every example asks you to predict before it shows you the output, because the
prediction is the exercise and the output is only the marking scheme. Where a
program prints more than is useful here, an excerpt is marked with ….
The programs are in examples/module-06/, each folder with its own
expected-output.txt. Build any of them with:
Example 1 — a hierarchy built from what is genuinely shared [L3 Modify]¶
The problem: three kinds of employee are paid three different ways, and all three have an identifier and a name. Move the shared facts into a base class without pretending the differences away.
Before you read on, predict: the program creates a SalariedEmployee, a
Contractor and a plain Employee, then calls a free function that takes
const Employee&. That function prints employee.monthlyPay() — and
SalariedEmployee has declared its own monthlyPay(). Which one does the free
function run, and does the number look right?
// examples/module-06/01-payroll-hierarchy/main.cpp
class Employee {
public:
Employee(int id, std::string name, long long monthlyPaisa)
: id_(id), name_(std::move(name)), monthlyPaisa_(monthlyPaisa) {}
// `virtual` is absent on purpose: without it, a call through an Employee&
// runs THIS function, whatever the object really is.
long long monthlyPay() const { return monthlyPaisa_; }
protected:
void setMonthlyPaisa(long long paisa) { monthlyPaisa_ = paisa; }
private:
int id_;
std::string name_;
long long monthlyPaisa_;
};
class SalariedEmployee : public Employee {
public:
// Note the name: `monthlyPay` here HIDES `Employee::monthlyPay`. It is not an
// override — the base function is not virtual.
long long monthlyPay() const { return monthlyPaisa() + bonus(); }
bool promote() { … setMonthlyPaisa(monthlyPaisa() + 500000); … }
private:
long long bonus() const { return static_cast<long long>(grade_) * 250000; }
int grade_;
};
Output (excerpt):
Three employees, three shapes:
#1 Ayesha Khan grade 2 pay 8500000
#2 Bilal Ahmed pay 5500000
#3 Chaudhry Sahab pay 4500000
The derived classes never wrote describe(), id() or name():
salaried.id() = 1
salaried.name() = Ayesha Khan
A function taking const Employee& sees only the base part:
ID card: #1 Ayesha Khan (base monthlyPay here would be 8000000)
The protected setter lets a derived class raise a salary:
before: grade 2 base 8000000 pay 8500000
promote() -> true grade 3 base 8500000 pay 9250000
…
promote() -> false grade 5 base 9500000 pay 10750000
Why it behaves that way: the base function printed 8000000 where the derived
object's own monthlyPay() would print 8500000. The free function was handed an
Employee&, the function is not virtual, and so the call was settled by the
compiler from the static type. The object is a SalariedEmployee; the code
cannot see that, and by the rules of this module it is not supposed to. Adding
virtual changes it, which is Module 7.
promote() is the second lesson. It raises the grade and calls the protected
setMonthlyPaisa — a derived class changing base state through a function the base
chose to expose to its family. It refuses at the top grade, so the last call returns
false and changes nothing, which is Module 4's invariant rule surviving into a
hierarchy.
What to take away: inheritance gives you the base's interface and data for
free, and gives you nothing at all in the way of polymorphism until you ask for it
with virtual.
Example 2 — public, protected, private: what actually changes [L4 Analyze]¶
The problem: settle, with a program rather than a claim, what the inheritance mode changes and what it does not.
Before you read on, predict: three derived classes — one public, one
protected, one private — all call the base's protected function. How many of
them compile? Then: which of them may be passed to a function taking
const Account&?
// examples/module-06/02-inheritance-modes-and-access/main.cpp
class Account {
public:
explicit Account(long long balancePaisa) : balance_(balancePaisa) {}
long long balance() const { return balance_; }
void deposit(long long paisa) { balance_ += paisa; }
protected:
// Available to any derived class, in any mode.
bool takeOut(long long paisa) {
if (paisa < 0 || paisa > balance_) return false;
balance_ -= paisa;
return true;
}
private:
long long balance_;
};
Output (excerpt):
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.
public inheritance — a CurrentAccount can be used as an Account:
account.balance() = 600000
withdraw(600000) -> true
withdraw(9000000) -> false <- more than the balance, refused, nothing changed
audit() sees an Account with balance 0
protected inheritance — the account is audited, and not an Account:
withdrawals() = 2
reportedBalance() = 600000
`savings.balance()` would not compile … `audit(savings)` would not compile either.
private inheritance — a ledger entry is built using an Account:
apply(+75000) = true
apply(-100000) = false -> more than the balance, refused
amount() after = 50000
`entry.deposit(1)` would not compile … `audit(entry)` would not compile either.
Why it behaves that way: all three derived classes reach takeOut and
balance, because access inside the hierarchy is a property of the member, not of
the mode. The mode changes what the rest of the world sees: protected inheritance
turns the inherited public interface into protected (so only a further deriver sees
it), and private inheritance turns it into private. Both also remove the is-a
relationship — audit(entry) fails to compile, and its failure message is worth
reading because it is the one this module's mistake table quotes:
What to take away: the mode does not restrict the derived class; it restricts the
world. public means is-a. The other two mean built using, and for "built using"
a member is usually clearer — which the program itself concludes in its last
paragraph.
Example 3 — the trace that settles every ordering question [L3 Modify]¶
The problem: three levels, four member objects, and no way to remember the order reliably from a rule you read. Print it.
Before you read on, predict: Manager's initialiser list writes reports_ and
desk_. Person declares second_ before first_, and its initialiser list writes
second_ first as well. Seven constructor-ish lines will print before
Manager body — write them down in order, then compare.
// examples/module-06/03-construction-and-destruction-order/main.cpp
class Person {
public:
explicit Person(std::string name)
: name_(std::move(name)),
second_(name_ + "'s second member"), // written in declaration order,
first_(name_ + "'s first member") { // which is what actually decides
std::cout << " Person body (name = " << name_ << ")\n";
}
private:
std::string name_;
Tracer second_; // declared second, so constructed second — whatever the
Tracer first_; // initialiser list says. Swap these two lines to see it.
};
class Employee : public Person {
public:
Employee(std::string name, int id)
: Person(std::move(name)), // 1. the base part, first of all
id_(id),
badge_(Person::name() + "'s badge") { … }
};
Output (excerpt):
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
Why it behaves that way: the two lists are one list read backwards, and each
list is generated by the same two rules — bases first (upwards), then members in
declaration order, then the constructor body. The order is not inherited from the
initialiser list's text, and the compiler proves it: writing first_ before
second_ produces
error: 'Person::first_' will be initialized after [-Werror=reorder]
error: 'Tracer Person::second_' [-Werror=reorder]
error: when initialized here
which is a warning the course promotes to an error, precisely so a wrong initialiser
order never reaches a student as a mystery. What the initialiser list can do for a
base is nothing at all: Person(...) is built first because it must be.
What to take away: "the derived destructor runs before the base is torn down" is the sentence this trace exists to make memorable — and in Module 7 it becomes the reason a virtual call inside a constructor is not a mistake you get to make.
Example 4 — hiding, ambiguity, and the one nobody reports [L4 Analyze]¶
The problem: three hierarchy defects that the compiler treats in three completely different ways.
Before you read on, predict: a Printer declares print(int),
print(std::string) and print(). A ColourPrinter adds print(std::string, bool).
Write down how many of the four are callable on a ColourPrinter before you look.
Output (excerpt):
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)
They do compile, because a qualified call names the class:
Printer::print(int) copies = 3
…
With `using Printer::print;` in the derived class:
Printer::print(int) copies = 3
Printer::print(string) label = essay
Printer::print() no arguments
FixedPrinter::print(string, bool) essay [colour]
2. Ambiguity — two bases both provide `id()`:
cyborg.Person::id() = 0
cyborg.Machine::id() = 9001
`cyborg.id()` would not compile: request for member 'id' is ambiguous.
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
Why it behaves that way: hiding is a rule about names, not signatures — one
print in the derived class removes all four from unqualified lookup, so the fix
restores names, not overloads. Ambiguity is a rule about lookup reaching two
equally good candidates, and the compiler reports it at the call. The diamond is a
rule about what the object contains: two base subobjects is a legal, deliberate,
unwarned-for layout, and each has its own copy of the base's state. There is no
error because there is nothing wrong with the request — you asked for two.
What to take away: when a hierarchy "loses" a function, think hiding; when a call will not compile with two candidates, think ambiguity; when one object gives two answers, count its subobjects. And notice which of the three is silent: the one that duplicates state.
Example 5 — the three-line attack on an inherited invariant [L4 Analyze]¶
The problem: a fixed-capacity stack, built by inheriting the storage from a general list type. Find out whether the capacity rule survives.
Before you read on, predict: FixedStack::push refuses the fourth element of a
capacity-3 stack. How many other ways can a caller add a fourth element to the
inheriting version? Name them before reading.
// examples/module-06/05-composition-over-inheritance/main.cpp
class IntList {
public:
void pushBack(int value) { values_.push_back(value); }
bool insertAt(int index, int value) { … }
void clear() { values_.clear(); }
int size() const { … }
bool at(int index, int& out) const { … }
bool removeAt(int index) { … }
};
class BrokenStack : public IntList { … }; // the tempting move
class FixedStack { IntList items_; … }; // the same capability, composed
Output (excerpt):
The same capability, inherited instead of composed:
push(4) -> false
stack [1 2 3] size 3
capacity() = 3, isFull() = true
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.
…
And the same attacks on the composed version, which would not compile:
stack.pushBack(99) error: 'class FixedStack' has no member
named 'pushBack'
Why it behaves that way: public IntList published every IntList operation
as part of BrokenStack's interface, and push's refusal is private to push.
The capacity rule was never an invariant of the object — it was a line in one
function. Composition does not fix the rule by enforcement either; it removes the
leak, because items_ is private and the only operations FixedStack offers are the
ones that respect the capacity.
What to take away: the question is never "can I reuse this implementation?" — you can, and composition does. The question is whether the derived type is the base type for every operation the base publishes. One operation that cannot be honoured means the answer is no.
Example 6 — using Base::Base;, and the three things it does not do [L3 Modify]¶
The problem: a Labelled document differs from a Document by one member.
Writing a constructor per base constructor is ceremony; is inheriting them an
option, and what is the catch?
Before you read on, predict: using Document::Document; is one line.
Labelled adds int revision_ = 1;. What value does revision_ have after
Labelled doc("Annual Report", 40); — and is there any way to pass a revision
through that constructor?
// examples/module-06/06-inheriting-constructors/main.cpp
class Document {
public:
explicit Document(std::string title) : title_(std::move(title)) { … }
Document(std::string title, int pages) : title_(std::move(title)), pages_(pages) { … }
};
class Inheriting : public Document {
public:
using Document::Document; // one line replaces a constructor per base overload
private:
int revision_ = 1; // default member initialiser, not a constructor argument
};
Output (excerpt):
The same two calls, through `using Document::Document;`:
Document(title) -> Annual Report pages 0
Document(title, pages) -> Annual Report pages 40
Annual Report / Annual Report (40 pages)
revision_ is 1 — the default member initialiser, NOT the base's constructor
Inherited constructors cannot reach a derived member. Proof: there is
no way to write `Inheriting("title", 40, 7)`. …
Three facts worth writing down:
1. `using Base::Base;` is all-or-nothing per constructor signature;
2. a constructor the derived class declares with the same signature
takes precedence — you can always override one of them;
3. copy and move constructors are NOT inherited.
Why it behaves that way: an inherited constructor is the base's constructor,
executed with a derived object as the target. It initialises the base part from its
parameters, then the derived members are initialised from their default member
initialisers — revision_ = 1 — because an inherited constructor has no way to
mention them. That is why the second half of the program writes a hand-written
Labelled(name, label) and shows both behaviours side by side: the hand-written one
sets the label, the inherited one leaves it empty.
What to take away: using Base::Base; is a genuine simplification exactly when
the derived class adds nothing initialisable from the constructor's arguments.
The moment a derived member needs a value, the ceremony is the design.
Next: Misconceptions and mistakes — eight wrong mental models, and the real messages this toolchain emits for each.