Skip to content

Module 6 — Misconceptions and Mistakes

Two different problems, fixed two different ways. A misconception is a wrong mental model, and only evidence removes it. A mistake is a concrete error, and information fixes it.

Every compiler message on this page was captured by compiling the snippets with g++ -std=c++17 -Wall -Wextra on the same toolchain the course examples are verified against. Line numbers refer to the snippets as shown here.


Part 1 — Misconceptions

Why this feels true. Inheritance is described in family language, and the class does get the base's data — so it looks as though there is a key.

Where it breaks. The int is in the object; the name is not usable.

class Base {
public:
    explicit Base(int id) : id_(id) {}
protected:
    void setId(int id) { id_ = id; }
private:
    int id_;
};

class Derived : public Base {
public:
    int read() const { return id_; }        // private in Base: inaccessible here
};
error: 'int Base::id_' is private within this context
   19 |     int read() const { return id_; }
      |                               ^~~
note: declared private here
   10 |     int id_;

The correct model. Access has two independent axes, and inheritance changes neither. A private member is reachable from the class that declares it and from nobody else — not from a derived class, not from a friend of a derived class. protected is the level that means "the family may reach it", and it is a deliberate choice with a cost (§3 of Concepts). If a derived class needs to change base state, the base must publish a function for it, exactly as setMonthlyPaisa does in the payroll example.

The re-test. Weeks from now: Base has protected int id_; and a friend class Auditor;. Which of the two — Derived or Auditor — may read it directly on a const Base&, and which must go through a getter?


"The initialization order is the order I wrote in the initialiser list" a rule half-learned from Module 3

Why this feels true. The initialiser list is the only place base and member construction is named, so it reads as an instruction.

Where it breaks.

error: 'Person::first_' will be initialized after [-Werror=reorder]
error:   'Tracer Person::second_' [-Werror=reorder]
error:   when initialized here

The correct model. Members are initialised in declaration order; bases are initialised first, in the order they are declared as bases, and no initialiser can move either. The list is a set of arguments, not a sequence. Reorder it to match declaration order and the warning goes away — which is also why the compiler warns rather than silently obeying: obeying the text is what a beginner expects, so the warning tells you your expectation is wrong.

The re-test. class D : public B1, public B2 { B1 x_; B2 y_; }. Write the construction order from memory, then check against examples/module-06/03-construction-and-destruction-order/.


"A derived object contains a copy of a base object next to its own members" a picture that is almost right

Why this feels true. "The book is a lendable item, so a book contains a lendable item" is a natural way to put it, and for single inheritance the picture predicts the right behaviour.

Where it breaks. With two bases that share an ancestor, the picture predicts something that does not happen — you would expect one shared base part, and you get two:

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.Tutor::name()   = Ayesha (staff record)   <- unchanged: a different Person

The correct model. A derived object contains the base subobject, laid out as part of the derived object — and multiple inheritance gives one subobject per base, so a diamond gives two of the shared ancestor. "One object" and "one copy of each thing" are different statements, and the diamond is where they part company.

The re-test. class D : public A, public B {} where both A and B inherit from Base which holds one std::string. How many strings does a D allocate, and how many does sizeof(D) account for?


"protected inheritance means the derived class can only touch protected base members" reading the mode as a filter on the class

Why this feels true. The keywords public, protected and private mean access levels one line earlier, so the same word in the same position looks like the same kind of rule.

Where it breaks. All three derived classes in examples/module-06/02-inheritance-modes-and-access/ call the base's public balance() and protected takeOut() successfully, in all three modes. What differs is only what the outside world sees:

public inheritance — a CurrentAccount can be used as an Account:
    account.balance()      = 600000
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.

The correct model. The mode describes the derived class's relationship to the world, not to its base: it reduces the accessibility the base's public and protected members have in the derived class. public = the relationship survives; protected = only further derivations see it; private = only this class does, and there is no is-a relationship at all.

The re-test. A class privately inherits from std::vector<int> and adds two operations. Is it a std::vector<int>? Can a caller holding it use size()?


"My derived class overrides the base's function" said about a function that is not virtual

Why this feels true. The two functions have the same name and the same parameters, and the derived one clearly gets called on a derived object.

Where it breaks. Through a reference to the base, the base one runs — and the program in §1 of Concepts prints the wrong salary because of it:

A function taking const Employee& sees only the base part:
  ID card: #1  Ayesha Khan   (base monthlyPay here would be 8000000)

The correct model. With no virtual, the derived monthlyPay hides the base's. Dispatch is settled by the static type of the expression — Employee& — at compile time. "Override" is a word with a definition, and it requires virtual plus an identical signature. This is the bridge to Module 7; until then, the honest description of Example 1 is "two functions, one hidden, no dispatch".

The re-test. Base declares void f(int); Derived declares void f(int). No virtual. Call d.f(3) on a Derived and b.f(3) on a Base& bound to d. How many different functions are in play?


"The diamond is a compile error I will be told about"

Why this feels true. Hiding and ambiguity are both reported, so the natural assumption is that the language reports hierarchy pathologies.

Where it breaks. The program's own heading for the third case: "and this one produces no diagnostic at all". The program compiles, runs, and prints two different names for one teaching assistant.

The correct model. The compiler only reports what it cannot resolve. Two subobjects are perfectly resolvable — you asked for two. It is a design bug with no compile bug, which is exactly the class of bug that gets shipped.

The re-test. What three questions would you ask about a class with two bases before deciding virtual inheritance is the answer? (See §7 of Concepts.)


"Inheriting gives me more than composing, so it is the stronger choice"

Why this feels true. Inheritance gives the whole base interface; composition gives one member. More capability reads as more value.

Where it breaks. The extra capability is exactly what broke the stack:

    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 … the rule is gone.

The correct model. An inherited public operation is part of your interface forever, and you must honour it in every derived type. Composition lets you publish four operations that mean one thing each. The question is not which gives more, but which of the base's promises you are willing to keep. See Effective C++, Item 38: "model 'has-a' or 'is-implemented-in-terms-of' through composition".

The re-test. Name one base class in the standard library it would be a mistake to inherit from, and one operation of it that a derived type could not honour.


Part 2 — Common programming mistakes

Symptom Cause Fix
error: no matching function for call to 'Base::Base()' The base has no default constructor and the derived constructor does not name one Add Base(args…) to the initialiser list
error: 'int Base::id_' is private within this context A derived class naming the base's private member Use the base's public/protected interface, or make the base publish a function
error: 'Account' is an inaccessible base of 'Ledger' The inheritance was written without public — class defaults to private Write : public Base, or stop inheriting and store a member
error: cannot derive from 'final' base 'Last' Deriving from a class marked final Derive from something else, or remove final — but ask why it was there
error: request for member 'id' is ambiguous Two bases provide the same name Qualify the call (Cyborg::Person::id()), or reconsider the design
error: no matching function for call to 'ColourPrinter::print(int)' Name hiding: one derived print removes all base prints Add using Printer::print;, or qualify the call
A member is initialised twice, or in the wrong order, or garbage appears The initialiser list order disagrees with declaration order Order it as declared; the -Wreorder warning names both members
A base member "disappears" in the derived class Hiding (see above), or a private base member being named from a derived class The two messages differ — read which one you have
One object gives two answers to the same question Two subobjects of a common ancestor (the diamond) virtual inheritance for interfaces, association for state
Callers can break a derived type's invariant with a base operation The derived type cannot honour every inherited public operation Compose instead of inheriting

Mistake 1 — forgetting to build the base [L3 Modify]

Reproduce it.

class Base {
public:
    explicit Base(int id) : id_(id) {}
    int id() const { return id_; }
private:
    int id_;
};

class Derived : public Base {
public:
    Derived() {                   // no base constructor called: there is no default one
        std::cout << "constructed\n";
    }
};

int main() { Derived derived; }

What you see.

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
m6a.cpp:1:7: note: candidate: 'constexpr Base::Base(const Base&)'
m6a.cpp:1:7: note: candidate: 'constexpr Base::Base(Base&&)'

Why. A derived object contains a base subobject, so the base must be built first. With no mention of the base, the compiler tries the base's default constructor, which explicit Base(int) suppressed by being declared. The candidate list that follows is the compiler enumerating every constructor it considered — including the implicitly declared copy and move constructors, which is why three "candidates expect 1 argument, 0 provided" notes appear rather than one.

Fix it.

class Derived : public Base {
public:
    Derived() : Base(7) { }        // one argument, one base
};

Prevent it. Write the base initialiser as the first thing in every derived constructor, before the body, in the order bases are declared. If a base has no default constructor, this is not optional — and if the base has one and the derived class should not use it, say so in a comment.


Mistake 2 — naming a private base member from a derived class [L3 Modify]

Reproduce it. Same file as Mistake 1, with one added line:

class Derived : public Base {
public:
    int read() const { return id_; }   // private in Base: inaccessible here
};

What you see.

error: 'int Base::id_' is private within this context
      |                               ^~~
note: declared private here

Why. private is access by class, not by relationship. The note: line is the useful half: it names the declaration, so you do not have to search for where the member lives.

Fix it. Either use the base's public accessor (id()), or — if derived classes genuinely need to write it — make the base publish a protected function with a name that states its purpose, as setMonthlyPaisa does.

Prevent it. Design the base's protected interface deliberately, and write the sentence the course's Module 4 recommends for every one of them: this exists so that a derived class can …. A protected member with no such sentence is either unnecessary or an unexamined coupling.


Mistake 3 — the missing public [L3 Modify]

Reproduce it.

class Ledger : private Account { … };      // or: class Ledger : Account { … }

void audit(const Account& account) { (void)account.balance(); }

int main() {
    Ledger ledger;
    audit(ledger);                          // was this supposed to compile?
}

What you see.

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

Why. Ledger privately inherits Account, so as far as the outside world is concerned there is no is-a relationship to convert through. Note where the error appears: at the call site, in a function that has nothing to do with the hierarchy — which is why this mistake is so often blamed on the wrong file. The message itself (is an inaccessible base of) is unambiguous once you know it.

Fix it. If the relationship really is is-a, write : public Account. If it is "built using", replace the inheritance with a member and say so in the type:

class Ledger {
public:
    long long amount() const { return amount_; }
private:
    long long amount_;              // an Account was never needed
};

Prevent it. Never write class D : Base. Name the mode every time, so its absence is visible as a deliberate error rather than an omission. And know the default: public for struct, private for class.


Mistake 4 — two bases with the same name [L4 Analyze]

Reproduce it.

class Person  { public: int id() const { return 1; } };
class Machine { public: int id() const { return 2; } };
class Cyborg : public Person, public Machine { };

int main() {
    Cyborg cyborg;
    return cyborg.id();            // which id()?
}

What you see.

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'

Why. Unqualified lookup found two equally good candidates. The compiler is reporting a design question, not an oversight: one object is claiming two identities.

Fix it. Qualify — cyborg.Person::id() — if two identities are genuinely what you want (they very often are: a lecturer who is also an adviser). If they are not, reconsider the relationship: the two roles are probably components of one thing, which is Module 9.

Prevent it. Do not let two bases publish the same operation name unless the two answers are both meaningful. If both are needed, name the operations for their roles — studentId(), staffId() — instead of overloading the same word.


Mistake 5 — the reorder warning, promoted to an error [L3 Modify]

Reproduce it. Write an initialiser list in a sensible reading order that differs from declaration order:

class Person {
public:
    Person(std::string name)
        : name_(std::move(name)), first_(…), second_(…) {}   // declared second_ first
private:
    std::string name_;
    Tracer second_;
    Tracer first_;
};

What you see.

error: 'Person::first_' will be initialized after [-Werror=reorder]
error:   'Tracer Person::second_' [-Werror=reorder]
error:   when initialized here

Why. The compiler initialises members in declaration order regardless of the list, and warns because the list is now telling a story that is not true. In this course every example and lab compiles with -Werror, so the warning fails the build. The dependency matters too: first_(…) and second_(…) may not use each other, because whichever is declared later is not yet constructed.

Fix it. Order the initialiser list exactly as the members are declared, and let the compiler be the check. If a member's value depends on another member's, that is the signal to move the computation into the constructor body, where both exist.

Prevent it. When you add a member, add it to the end of both the declaration list and the initialiser list, and reorder the initialiser list only when you change the declaration order deliberately.


Mistake 6 — deriving from final [L1 Understand]

Reproduce it.

class Last final { public: virtual int value() const { return 1; } };
class MoreLast : public Last { … };

What you see.

error: cannot derive from 'final' base 'Last' in derived type 'MoreLast'
   18 | class MoreLast : public Last {
      |       ^~~~~~~~

Why. final is a promise made by the base's author. The error is the promise being kept — and it is the good outcome, because the alternative is a derived class whose behaviour the base cannot guarantee.

Fix it. Derive from a base that is meant to be derived from, or remove final if you also own Last and can state why further derivation is now safe.

Prevent it. Reach for final when an operation must not be replaced — a checksum, a serialisation format, a destructor pair — and write the reason in a comment next to it. A final nobody can explain is usually a design that has not been thought through.


Next: Debugging — the subobject map, and a diagnosis that takes four steps.