Skip to content

Module 7 — 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 produced by compiling the snippet shown, with g++ -std=c++17 -Wall -Wextra on the toolchain the course's examples are verified against.

This module has more misconceptions than mistakes, because more of its failure modes are silent. Three of the traps below produce no diagnostic at all.


Part 1 — Misconceptions

"If the object is a Derived, the Derived function runs" the belief that costs the most

Why this feels true. It is true when you call the function on the object by name. plainBike.dailyRate() really does return the motorbike's rate — the compiler knows the object's exact type and does the obvious thing.

Where it breaks. The moment the object arrives through the base type, examples/module-07/01-virtual-or-not/main.cpp shows the difference:

  plainBike.dailyRate()   = 60000   <- the motorbike's own function
  plainBike through PlainVehicle&    = 200000   <- WRONG. Static type decides
  A fleet of one car and one motorbike, three days each:
    non-virtual total = 1200000   (expected 780000)

The correct model. The function called is decided by the static type of the expression, not by the object. Dynamic dispatch needs virtual and a reference or pointer — two conditions, both required. virtual does not change what a call on a named object does; it changes what a call through the base does, which is where all the interesting loops are.

The re-test. Derived d; Base& b = d; — call d.f() and b.f() where f is virtual. How many distinct functions are involved, and which one runs in each case?


"virtual makes my program slower, so I should avoid it"

Why this feels true. It is true that a virtual call cannot always be inlined, and that every polymorphic object carries a hidden pointer. The program measures both:

  sizeof(TaggedShape) = 24 bytes
  sizeof(Shape)       = 8  (the hidden pointer alone — the vtable pointer)

Where it breaks. The comparison that matters is not "dispatch versus a direct call". It is "one new class" versus "a new enumerator, a new case in three separate switches, and a decision about which field carries the side" — as examples/module-07/06-switch-to-dispatch/main.cpp prints. The cost of the virtual version is a pointer per object and an indirection per call; the cost of the tagged version is paid by every future change, in every file that switches on the tag.

The correct model. Virtual dispatch costs a pointer and an indirection. It buys dispatch that cannot disagree with itself, and the ability to add a kind without editing existing code. Measure before optimising it away: if a call is made millions of times per second and cannot inline, you will see it in a profile. sizeof and a profiler are evidence; "virtual is slow" is a slogan.

The re-test. Name one place in this course's running system where you would not make a function virtual, and say what you would measure to justify that.


"A destructor declared virtual is what makes the object polymorphic"

Why this feels true. The destructor is the one place where beginners are told to write virtual almost as a ritual, and the ritually repeated word starts to look like the thing that enables dispatch.

Where it breaks. examples/module-07/01-virtual-or-not/main.cpp writes virtual ~PlainVehicle() = default; and dailyRate() is still not virtual, so the fleet total is still wrong. A virtual destructor does not make anything else dispatch — virtual is per-function.

The correct model. virtual applies to the function it is written on, and to that function's overrides. The base destructor is virtual for one reason only: so that delete basePointer runs the whole destruction chain (§4 of Concepts). Write it on every base class anyway, because a base class with no virtual functions today usually has one tomorrow.

The re-test. class Base { public: virtual ~Base() = default; void f(); }; — how many of Base's functions are virtual?


"The destructor question only matters if I use new and delete"

Why this feels true. Modern C++ avoids raw new, so a rule about delete looks like advice about code you no longer write.

Where it breaks. The same mismatch happens wherever a derived object is destroyed through a base handle:

std::unique_ptr<Base> p = std::make_unique<Derived>();   // same question, new syntax

The deleting unique_ptr does delete p on a Base*. If ~Base() is not virtual, the derived part is not destroyed. Smart pointers remove the need to remember delete; they do not remove the need for a virtual destructor.

The correct model. The rule is about the static type used to destroy, not about the syntax of the deletion. Any class with a virtual function and a base that might be used to destroy it needs virtual ~Base(). Module 11 shows that ownership through smart pointers removes a different failure mode — leaking on an exception path — and that both rules apply at once.

The re-test. class Base { virtual void f(); }; with no declared destructor. Is ~Base() virtual? What does the compiler generate, and what happens on delete basePointer?


"Slicing is a bug the compiler will report"

Why this feels true. It deletes data. Languages that report everything report data loss.

Where it breaks. examples/module-07/04-slicing/main.cpp:

    byValue    -> person: Ayesha Khan   [sizeof 40]
    The byValue line is not a bug in `label()`: the parameter really is
    a Person. `byValue` constructed one, so the virtual call had only a
    Person to dispatch to …

The correct model. byValue asked for a Person, and it got a perfectly valid Person. Nothing is wrong with the copy — it is exactly what the code specified. The mistake is in the interface, so that is where the fix goes: parameters are const Base&, Base* or a smart pointer; containers hold handles rather than objects.

The re-test. void f(std::vector<Base> v) is called with two Derived objects. What does f see, and what would sizeof tell you?


"A virtual call in the constructor calls the derived version"

Why this feels true. The mechanism is described as "the object's type decides", and the object is a Derived — you wrote Derived d;.

Where it breaks. The program prints the four answers to one virtual call:

  Base()       calls describe() -> base (nothing more is known yet)
  Derived()    calls describe() -> derived (all my members are ready)
  ~Base()      calls describe() -> base (nothing more is known yet)

The correct model. Dispatch needs a complete object. During a base constructor the derived part does not exist yet, so the object's dynamic type is the class currently being constructed; during a base destructor it is again. The language is not being unhelpful; dispatching to a derived override would read uninitialised members. And if the function called is pure virtual, the behaviour is undefined — the compiler warns if it can:

m7b.cpp: In constructor 'Base::Base()':
m7b.cpp:5:28: warning: pure virtual 'virtual std::string Base::describe() const' called from constructor
    5 |     Base() { (void)describe(); }
      |                    ~~~~~~~~^~

That warning is not a promise of any particular runtime outcome. In this course's toolchain, running the program produced no output and an exit status that was not the same on every run of the same binary — which is what undefined behaviour looks like from the outside, and why the rule is "do not do it" rather than "look out for this message".

The re-test. A base constructor needs a name that only the derived class can supply. Name two ways to get it without calling a virtual function.


"override is optional style"

Why this feels true. The program compiles without it, and the derived function is still virtual because the base's is.

Where it breaks. Without override, a signature that differs by one character compiles silently as a new function that hides the base's, and the base's behaviour keeps running everywhere a base handle is used. With it:

error: 'std::string WrongName::Name() const' marked 'override', but does not override
error: 'std::string WrongConst::name()' marked 'override', but does not override
warning: 'virtual std::string Logger::name() const' was hidden [-Woverloaded-virtual=]
error: 'void WrongParam::log(const char*)' marked 'override', but does not override

The correct model. override generates no code and costs nothing. It converts one of the language's quietest mistakes into a build failure. Write it on every function you intend to override; the compiler will tell you when your intent and your signature disagree.

The re-test. Base has virtual void f(int). Derived has void f(int) override;. Now change the base to virtual void f(long). What happens, and what would have happened without override?


"An override adds to the base's behaviour"

Why this feels true. "Override" sounds like a layer you put on top, and in some languages the base is called automatically.

Where it breaks. examples/module-07/03-override-and-final/main.cpp prints both consequences side by side:

  TaggedLogger kept the base's work by calling Logger::log explicitly:
    [warn] billing | payment declined
  UpperCaseLogger did not, so `[warn]` survives only because it wrote
  the brackets itself …:
    [warn] PAYMENT DECLINED

UpperCaseLogger did not "extend" the base's formatting; it replaced it, and the only reason the output still looks formatted is that it reimplemented the format.

The correct model. An override replaces the base implementation. To keep the base's work, call it explicitly — Logger::log(level, message) — and remember that the call is qualified, so it is not dispatched again. This is the pattern behind the non-virtual interface technique and the template-method pattern of Module 15.

The re-test. A derived save() wants to validate and then reuse the base's save(). Write the one line that does it, and say why the qualified call is required rather than merely tidy.


Part 2 — Common programming mistakes

Symptom Cause Fix
A base function runs where the derived one should The function is not virtual, or the call is made on an object rather than a handle Declare virtual in the base; pass Base&, Base* or a smart pointer
error: … marked 'override', but does not override Signature mismatch: name, parameters, const, or return type Match the base exactly, or drop override if a new function was intended
warning: … was hidden [-Woverloaded-virtual=] A derived function hides a virtual base function Add override after fixing the signature, or using Base::f; if both are wanted
error: cannot derive from 'final' base 'X' Deriving from a final class Derive from something else; ask why final was written
error: … overriding final function Overriding a function marked final in the base Remove the override, or revisit whether final belongs there
Data disappears when a derived object is passed or stored Slicing: a copy of the static type was made const Base&, Base*, std::unique_ptr<Base>
Only the base destructor runs; resources leak The base destructor is not virtual virtual ~Base() = default;
A virtual call in a constructor or destructor runs the base version Correct behaviour: the derived part does not exist yet, or no longer does Do not call virtual functions there; pass data up, or split construction into two steps
warning: pure virtual … called from constructor A pure virtual function invoked from a constructor The call is undefined behaviour; remove the call and restructure
Everything "works", and the numbers are wrong Learning the module from output that was wrong in a way nobody flagged Predict first, then run; check virtual, handles and signatures explicitly

Mistake 1 — the missing virtual [L3 Modify]

Reproduce it. Two vehicles, one rate function, no virtual, and a loop through base references:

class PlainVehicle { public: long long dailyRate() const { return 200000; } };
class PlainMotorbike : public PlainVehicle { public: long long dailyRate() const { return 60000; } };

long long bill(const PlainVehicle& v, int days) { return v.dailyRate() * days; }

What you see. No diagnostics at all. bill(motorbike, 3) returns 600000 instead of 180000, and a fleet total of 1200000 where 780000 was expected.

Why. The call is bound from the static type of the expression v, which is const PlainVehicle&. The object behind it is never consulted. Nothing is illegal: it is a direct call to a function you named.

Fix it. Declare the base function virtual:

class PlainVehicle {
public:
    virtual ~PlainVehicle() = default;
    virtual long long dailyRate() const { return 200000; }
};

Prevent it. When you write a base class, decide per function whether subclasses may replace it, and write virtual where the answer is yes. Then pass base objects as references or pointers, never by value — because slicing produces the same symptom with a different cause.


Mistake 2 — a signature that differs by one character [L3 Modify]

Reproduce it.

class Logger {
public:
    virtual ~Logger() = default;
    virtual std::string name() const { return "logger"; }
};

class WrongConst : public Logger {
public:
    std::string name() override { return "not const"; }   // const missing
};

What you see.

error: 'std::string WrongConst::name()' marked 'override', but does not override
warning: 'virtual std::string Logger::name() const' was hidden [-Woverloaded-virtual=]
note:   by 'std::string WrongConst::name()'

Why. A function "overrides" only when name, parameter list, const-qualification and return type all match. Miss one and you have declared an unrelated function that hides the base's, because Module 6's hiding rule is about names.

Fix it. Match the declaration exactly — std::string name() const override — and let the compiler confirm it.

Prevent it. Write override at the moment you write the derived declaration, not afterwards. If the base is yours, consider making the whole class or the individual function final when replacement would be a mistake, so the check happens the other way round:

error: virtual function 'virtual std::string SealedChild::auditLine() const' overriding final function
note: overridden function is 'virtual std::string Sealed::auditLine() const'

Mistake 3 — deleting through a base pointer without a virtual destructor [L3 Modify]

Reproduce it.

Base* p = new Derived;     // Derived allocated 4096 bytes
delete p;                  // ~Base() is not virtual

What you see. In the measured program: ~Base ran, the buffer count stays at 1, and 4096 bytes are never freed. There is no compiler message — a modern compiler will warn if the class has other virtual functions and you ask for -Wnon-virtual-dtor, but the course's examples show the measurement because the rule must be believed, not merely trusted.

Why. The standard makes it undefined behaviour: destroying a derived object through a base pointer with a non-virtual destructor. The consequence is that the derived destructor does not run, so whatever it owned is not released. Undefined behaviour is why the program refuses to promise a symptom and why a sanitizer reports a leak where another compiler might crash.

Fix it.

class Base {
public:
    virtual ~Base() = default;      // one line, on every base class
    virtual void f();
};

Prevent it. Put virtual ~Base() = default; in every base class as you write it, before you have virtual functions to justify it. If you want the mistake to be impossible rather than merely unlikely, make the base's destructor protected and non-virtual — then delete p is a compile error, and a smart pointer (Module 11) is the only way to destroy the object.


Mistake 4 — relying on a virtual call in a constructor [L4 Analyze]

Reproduce it.

class Base {
public:
    Base() { (void)describe(); }                       // pure virtual during construction
    virtual ~Base() = default;
    virtual std::string describe() const = 0;
};

class Derived : public Base {
public:
    std::string describe() const override { return "derived"; }
};

What you see. A warning if the compiler can see it:

m7b.cpp: In constructor 'Base::Base()':
m7b.cpp:5:28: warning: pure virtual 'virtual std::string Base::describe() const' called from constructor

and, at run time, undefined behaviour — no message, and in this course's toolchain an exit status that was not even consistent between two runs of the same binary.

Why. The object's dynamic type during Base()'s body is Base, and Base declares describe() pure. There is no implementation to run, so the program is not required to do anything in particular.

Fix it. Remove the call. If the base needs derived data, take it as a constructor argument; if it needs derived behaviour, split construction — an initialise() step called by the caller after the object exists, or a factory function (Module 15) that constructs and then configures.

Prevent it. Treat "the base class calls a virtual function" as a design smell worth a comment even when the function has a base implementation and therefore "works". A template-method design that needs derived behaviour belongs in a non-virtual public function called after construction — which is exactly the non-virtual interface pattern Module 8 introduces.


Mistake 5 — slicing at a call site [L3 Modify]

Reproduce it.

void byValue(Person person);          // ← the parameter is an object, not a reference

Manager manager("Ayesha Khan", "Data Science", 7);
byValue(manager);

What you see. No diagnostics, and output that describes a Person:

    byValue    -> person: Ayesha Khan   [sizeof 40]

Why. The parameter is an object, so a copy is constructed — of the static type Person. The team and the report count were never copied, so the virtual call has only a Person to dispatch to. The copy is not a bug; the interface is.

Fix it.

void byReference(const Person& person);      // or Person*, or a smart pointer

Prevent it. Two habits: parameters of polymorphic types are always references or pointers, and containers of polymorphic types hold handles, not objects. Both are greppable rules — search for (Person and vector<Person> — which is the kind of rule worth having, because the compiler will not enforce either one.


Next: Debugging — the declared-type test, and a diagnosis that starts by printing sizeof.