Skip to content

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:

  1. Distinguish compile-time polymorphism (overloading and templates) from run-time polymorphism (virtual functions), and choose between them.
  2. Declare a virtual function, override it in a derived class, and explain what "override" means precisely.
  3. Use override to have the compiler verify that an override really overrides, and final to stop further overriding.
  4. Explain static versus dynamic binding, and what makes dispatch dynamic.
  5. Call through a base-class reference or pointer and get derived behaviour.
  6. Produce object slicing deliberately, explain exactly what was lost and why, then fix it with references or pointers.
  7. Explain why a polymorphic base class needs a virtual destructor, and describe what goes wrong without one.
  8. Predict what a virtual call during construction or destruction actually calls, and explain why.
  9. 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.
  10. Replace a switch on an object's type with polymorphic dispatch, and compare the two versions.
  11. Use a container of standard smart pointers to hold polymorphic objects.

Core concepts

  • Polymorphism — one interface, more than one behaviour.
  • virtual function — 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 what override is 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 virtual and 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 virtual so that delete basePointer destroys 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 switch statement 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.
  • virtual destructors and noexcept meet 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 switch on 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 virtual would 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.