Skip to content

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 Account buy? Almost nothing that a member would not buy more clearly: LedgerEntry needs an amount, and a lone long 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:

  1. Bases before derived, all the way up — Person, then Employee, then Manager.
  2. Within one class, members in declaration order, before that class's own constructor body. second_ is declared before first_, 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.
  3. 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:

                Person
               ▲      ▲
        Student       Tutor
               ▲      ▲
            TeachingAssistant
class TeachingAssistant : public Student, public Tutor { };

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

error: cannot derive from 'final' base 'Last' in derived type 'MoreLast'

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 FixedStack is an IntList? No — you cannot insert at an arbitrary index, or clear a stack, and still be a stack. A FixedStack is implemented using an IntList? 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:

  1. 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.
  2. Does the derived type genuinely is the base? "A Manager is an Employee" — yes. "A Car is an Engine" — no, however much code it saves.
  3. 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.

  1. Derived derives from Base, which has a protected member p_ and a private member q_. Which of the two can Derived's member functions name directly, and which can they reach only through a base function?
  2. 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.
  3. A base class declares void print(int); a derived class declares void print(std::string). A caller writes d.print(3). What happens, why, and what is the smallest fix?
  4. Two bases both derive from Person. How many name_ strings does TeachingAssistant contain, and which one changes when you call ta.Student::rename("…")?
  5. You are asked for a Deque that reuses a Vector'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.