Skip to content

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:

g++ -std=c++17 -Wall -Wextra main.cpp -o program && ./program

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:

error: 'Account' is an inaccessible base of 'Ledger'
   17 |     audit(ledger);
      |     ~~~~~^~~~~~~~

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.