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:
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:
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.
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:
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.
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.