Module 7 — Polymorphism¶
After this module you can design one interface and vary the behaviour behind it at run time — and you can diagnose the three traps that catch almost everyone: slicing, the missing virtual destructor, and virtual calls during construction.
At a glance¶
| Prerequisites | Module 6 |
| Estimated study time | 9–11 hours |
| Lab | Lab 7 — one interface, many behaviours; slicing reproduced then fixed |
| Exercises | Three tiers — warm-up, core, challenge |
| Cognitive levels | L3 Modify → L6 Build — exit level L5 Design |
| Self-assessment | Module quiz |
Why this module matters¶
Inheritance shares code. Polymorphism is what makes the sharing useful: the ability to write code against one interface and have the correct behaviour chosen at run time, for objects whose concrete type you may not even know when the code is written.
That property is what makes extensible software possible. A loop that prints every
item in a catalogue will keep working when a new kind of item is added — if it
was written against the interface. Without it, adding a type means finding and
editing every switch on item type, and one missed case is a silent bug.
Polymorphism is also where C++ differs sharply from most languages, because the
choice of how something is dispatched belongs to the programmer. virtual is
opt-in, and the consequence is three traps that every C++ programmer meets:
- Object slicing — copying a derived object into a base object silently discards the derived part, and the behaviour with it.
- The non-virtual destructor — deleting a derived object through a base pointer without a virtual destructor is undefined behaviour.
- Virtual calls during construction or destruction — they do not dispatch to the most-derived type, because at that moment it does not exist yet, or no longer exists.
You will reproduce each one deliberately, observe it, and fix it. Bugs you have caused on purpose are bugs you recognise instantly later.
Learning objectives¶
By the end of this module you will be able to:
- Distinguish compile-time polymorphism (overloading and templates) from run-time polymorphism (virtual functions), and choose between them.
- Declare a
virtualfunction, override it in a derived class, and explain what "override" means precisely. - Use
overrideto have the compiler verify that an override really overrides, andfinalto stop further overriding. - Explain static versus dynamic binding, and what makes dispatch dynamic.
- Call through a base-class reference or pointer and get derived behaviour.
- Produce object slicing deliberately, explain exactly what was lost and why, then fix it with references or pointers.
- Explain why a polymorphic base class needs a virtual destructor, and describe what goes wrong without one.
- Predict what a
virtualcall during construction or destruction actually calls, and explain why. - Describe, at a conceptual level, how dynamic dispatch is typically implemented (a virtual function table per class, a hidden pointer per object) and state that this is a typical implementation, not a language guarantee.
- Replace a
switchon an object's type with polymorphic dispatch, and compare the two versions. - Use a container of standard smart pointers to hold polymorphic objects.
Core concepts¶
- Polymorphism — one interface, more than one behaviour.
virtualfunction — a member function whose implementation is selected at run time from the object's actual type.- Override — a derived function with the same name, parameter list and
const-qualification as a virtual base function. A different signature creates a different function that hides the base's — the most common silent mistake, which is exactly whatoverrideis for. override— tells the compiler to check that this function really does override something, and to fail the build if it does not.final— on a function, forbids further overriding; on a class, forbids deriving from it.- Static binding — the compiler decides which function to call, from the static type of the expression. The default.
- Dynamic binding — the compiler emits a lookup, resolved at run time from
the object's dynamic type. Requires
virtualand a call through a reference or pointer. - Base reference or pointer — the vehicle for dynamic dispatch. Without one, polymorphism cannot happen.
- Object slicing — initialising a base-class object from a derived-class object copies only the base part. The derived data and behaviour are gone, and the object is still a valid base object, so nothing complains.
- Virtual destructor — a destructor declared
virtualso thatdelete basePointerdestroys the whole derived object. Any class with virtual functions that may be deleted through a base pointer needs one. - Construction and destruction dispatch — inside a constructor or destructor, a virtual call resolves to the class currently being constructed or destroyed, not the most-derived type.
- vtable / vptr — the usual implementation: one table of function pointers per class, and a hidden pointer in each object. A typical implementation, not a rule the standard imposes.
- Polymorphic container — a container of base-class pointers, typically
std::vector<std::unique_ptr<Base>>, so different derived objects live together. Ownership is the subject of Module 11.
Module contents¶
| Section | What it contains |
|---|---|
| Introduction (this page) | Overview, prerequisites, objectives, concept inventory |
| Concepts and explanations | Static versus dynamic binding, virtual and override, final, how dispatch works, virtual destructors, slicing, dispatch in constructors, the switch-to-dispatch refactor, when not to use virtual |
| Worked examples | Six programs — the same fleet with and without virtual, a measured leak, override and final, slicing measured with sizeof, a four-answer dispatch trace, and a tagged struct refactored into a hierarchy |
| Code examples | examples/module-07/ — six programs, each with captured output, including deliberate misbehaviour |
| Misconceptions and mistakes | Eight wrong mental models, ten symptoms, and five worked mistakes with their real compiler messages |
| Debugging | The declared-type test, the three silent failures to eliminate first, and a diagnosis that starts with sizeof |
| Exercises | Three tiers, including a program with three defects — two of them silent — and a missing virtual to find |
| Lab 7 | The motor pool: an interface, ownership that cannot slice, the destructor rule measured, and the broken version recorded on purpose |
| Challenge problems | A diagnosis to write as a review comment, a media-player interface, and the memo on when not to use virtual |
| Self-assessment | Ten questions, including three predictions and a hierarchy with four defects |
| Summary | What must be retained, the terms to be able to define, and the checklist |
| Further reading | Books and official documentation |
What you can do after this module¶
- Write code that operates on base-class references and exhibits each derived type's behaviour.
- Demonstrate slicing, explain what was lost, and fix it.
- Spot a polymorphic base class with no virtual destructor, and say what will go wrong.
- Explain why a virtual function called from a constructor does not do what a beginner expects.
- Replace a type-switching
switchstatement with dispatch, and articulate the gain in maintainability.
Where this is used later¶
- Abstract interfaces — pure virtual functions and classes that cannot be instantiated — are Module 8.
- Ownership of polymorphic objects in containers is Module 10 and Module 11.
- Container choices for polymorphic objects use the algorithms of Module 14.
virtualdestructors andnoexceptmeet exception safety in Module 13.- Strategy and template-method patterns in
Module 15 are refined forms of the
switch-to-dispatch refactor you do here.
Common mistakes to watch for¶
| Symptom | Cause |
|---|---|
| The base version of a function is always called | The function is not virtual, or the call is made on an object rather than a reference or pointer |
error: 'void Shape::draw()' marked 'override', but does not override |
The signature differs from the base: a missing const, a different parameter type, or a typo in the name |
| Only the base part of an object is ever used | Object slicing — the object was copied by value into a base object |
| A destructor runs but the derived object's resources leak | The base destructor is not virtual |
| Derived classes fail to compile, complaining about hidden overloads | Name hiding: a derived f(int) hides a base f(std::string). Add using Base::f; or qualify |
| A virtual call in a constructor calls the base's version | Correct behaviour: during construction, the derived part does not exist yet |
Warning: delete on a polymorphic class with a non-virtual destructor |
Exactly the problem above; the compiler is warning about undefined behaviour |
Skills checklist¶
Levels this module works at: [L3 Modify → L6 Build] — see How you will learn.
Tick an item only when the evidence exists:
- [ ] I can say which function runs at any call site, by naming the static type of the expression — evidence: the declared-type test completed for the slicing program
- [ ] I can make a hierarchy dispatch correctly with one keyword, and say why that keyword changes the behaviour of code I never touched — evidence: C1's corrected fleet total, diffed against the original output
- [ ] I can reproduce object slicing on purpose and fix it — evidence: the recorded wrong output and the container of handles that replaced it
- [ ] I can explain why a polymorphic base needs a virtual destructor, in terms of what runs, not in terms of a rule — evidence: the leak counter returning to zero
- [ ] I can predict a virtual call's behaviour at four points in an object's lifetime — evidence: your written trace for W3
- [ ] I can replace a
switchon a type tag with dispatch and state what the refactor cost — evidence: X2's two edit counts, size table and timings - [ ] I can name three places where
virtualwould be the wrong choice, with a reason each — evidence: your X3 memo, with its two greppable review rules
Suggested next step¶
Previous: Module 6 — Inheritance · Next: Module 8 — Abstract Classes · All modules: Modules index
Module 8 assumes your dispatch works and your destructors are virtual. What it adds is the step this module kept deferring: a base class that cannot be instantiated, so that "every one of these implements the contract" is a statement the compiler enforces — and the running system loses the base field that has been meaningless for half its derived classes since Module 6.