C++ Part I · Module 17 · Runtime Polymorphism
override and final
A virtual function is matched to the one it overrides by signature, and the match has to be exact: the same name, the same parameter types, the same constness, a compatible return type. Get any of that wrong and you have not written an override. You have written a new function that hides the base one, which Chapter 16.6 measured as a silent redirection and which is worse inside a polymorphic hierarchy, because the base's implementation keeps running through every base reference and pointer — so the code you carefully wrote is simply never reached, while everything compiles and the program produces answers. This chapter measures that happening from a single character: unsigned where the base said int. Then the keyword that converts it into a compile error, and the four distinct mistakes it catches with four distinct messages. The chapter also records something that has not happened in a while. Across the two previous modules, seven separate findings produced no diagnostic at any warning level, which became a reasonable working assumption. This failure breaks the pattern: clang warns about it by default, in -Wall, with a note naming the exact parameter that mismatched. The keyword is still worth writing — a warning is not an error, and it does not cover every case — but the honest report is that the tooling is on your side here for once. The chapter closes with final, which stops a hierarchy deliberately and has a measurable second effect on the generated code.
Intermediate18 min readC++Part IFundamentalsPolymorphism
Part I · Module 17 · Chapter 17.3 · override and final
1. The Match Has to Be Exact
For a derived function to override a base virtual function, these must agree:
- the name, exactly;
- the parameter types, exactly — no conversions considered;
- the constness of the member function;
- the ref-qualifier, if any;
- and the return type must be identical or covariant (Chapter 17.2 §9's note).
Miss any of them and you have declared a different function, which — having the same name — hides the base's by Chapter 16.6 §3's rule.
2. The Mistake
struct Base { virtual ~Base() = default; virtual void run(int n) const { /* ... */ } };
struct Derived : Base { void run(unsigned n) const { /* ... */ } }; // unsigned, not intOne character. Measured:
through a Base&: Base::run(int) 7
through Derived: Derived::run(unsigned) 7Through a Base&, the base's implementation ran. Derived::run exists, compiles, and is unreachable from every piece of polymorphic code in the program — which is all of it, because the whole point of the hierarchy is that callers hold base references.
This is strictly worse than Chapter 16.6 §4's non-virtual hiding. There, a reader looking at the derived type at least got the derived function. Here the derived function is dead code that looks live.
3. For Once, the Compiler Warns
This is worth stating plainly because it breaks a pattern. Across Chapters 15.4, 16.2, 16.4, 16.6 and 16.8, seven separate failures produced no diagnostic at any warning level — enough repetition that "assume nothing warns" had become the working assumption.
Not here:
warning: 'Derived::run' hides overloaded virtual function [-Woverloaded-virtual]
note: hidden overloaded virtual function 'Base::run' declared here:
type mismatch at 1st parameter ('int' vs 'unsigned int')The note names the exact parameter and both types. And it is on by default in a normal build:
-Wall -> 1 warning [-Woverloaded-virtual fired]
-Wextra -> 0 warnings
-Wall -Wextra -Wpedantic -> 1 warning [-Woverloaded-virtual fired]-Woverloaded-virtual is in -Wall, not -Wextra — worth knowing precisely, because a project building with only -Wextra would miss it.
It also catches the missing-const case, which is not an overload clash at all:
nooverride2.cpp (void run(int) vs virtual void run(int) const):
-Wall -Wextra -Wpedantic -> 1 warning [caught]4. Why override Is Still Worth Writing
Four reasons, given that §3's warning exists.
A warning is not an error. It can be suppressed, lost in build output, or disabled by a project that treats -Wall as advisory. override makes the mistake a hard failure.
It documents intent. A reader seeing override knows the function is part of the base's interface without checking the base.
It catches the reverse mistake too — removing virtual from a base function, or renaming it, breaks every derived class loudly instead of silently reverting them to hiding.
And it costs nothing — no runtime effect, no size change.
5. Measured: What Happens When the Base Changes
§4's third claim — that override protects you when the base changes — is worth measuring rather than asserting, because it covers a case §3's warning does not.
One source file, a derived class that writes run(int) const and is never edited, and three plausible changes to the base underneath it:
// the base, in three later versions
virtual void execute(int n) const; // renamed
void run(int n) const; // `virtual` removed
virtual void run(long n) const; // int -> long base change WITHOUT override WITH override
none compiles (0w) compiles (0w)
renamed ERROR ERROR: only virtual member functions …
virtual removed compiles (0w) ERROR: only virtual member functions …
signature int->long compiles (1w) ERROR: non-virtual member function …Three rows to read, in increasing order of interest.
Renamed is a hard error either way — but only because the caller in this program names run(). That is not override doing work, and the honest reading is that a rename is caught by whoever calls the old name, which may be nobody.
Signature changed compiles without override and draws one warning — §3's -Woverloaded-virtual, doing its job. With override it is an error.
And virtual removed from the base compiles with zero warnings. No diagnostic at all: the base function is now non-virtual, the derived function hides it, and -Woverloaded-virtual has nothing to say because there is no longer a virtual function being hidden. With override, it is a hard error naming the cause.
That last row is the whole argument for the keyword. It is the Chapter 16.8 §2 fragile-base problem applied to a hierarchy's behaviour rather than its interface: somebody removed one word from a base class, every derived override silently stopped overriding, and the only thing standing between that and a shipped program is a keyword in files the base's author never opened.
6. The Same Mistake, With override
struct Derived : Base { void run(unsigned) const override { } };error: non-virtual member function marked 'override' hides virtual member functionAn error, not a warning. The build stops.
7. The Four Mistakes It Catches
Each with override written, and each producing a different message:
| Derived declaration | Diagnostic |
|---|---|
void run(unsigned) const override | non-virtual member function marked 'override' hides virtual member function |
void run(int) override (missing const) | non-virtual member function marked 'override' hides virtual member function |
void runn(int) const override (typo) | only virtual member functions can be marked 'override' |
int run(int) const override (return type) | virtual function 'run' has a different return type ('int') than the function it overrides (which has return type 'void') |
void run(int) const override | compiles |
The third message is the one to learn, because it is slightly misleading on first reading: only virtual member functions can be marked 'override' means "nothing in any base matches this signature", not "you cannot use override here".
One more case, met in practice while writing Chapter 17.5's program:
struct BadDerived : BadBase { ~BadDerived() override { /* ... */ } }; // base dtor not virtualerror: only virtual member functions can be marked 'override'Writing override on a destructor is a cheap way to assert that the base's destructor is virtual — and here it correctly refused, because it was not.
8. Where to Write It
On every derived virtual function. Without exception.
And not virtual as well. Writing both is legal and redundant:
void run(int) const override; // preferred
virtual void run(int) const override; // redundant; `override` implies itThe derived function is virtual regardless — Chapter 17.2 §1's inheritance of the keyword. Writing virtual again adds nothing and costs a reader a moment wondering whether it means something.
9. final on a Function
final says: no further derived class may override this.
struct Mid : Base { void run() const final { } };
struct Leaf : Mid { void run() const override { } }; // refusederror: declaration of 'run' overrides a 'final' functionTwo uses. Stating that a behaviour is settled — this implementation is the answer and further derivations must not change it, usually because other members depend on what it does. And closing a customisation point that was useful one level down and should not keep being re-opened.
10. final on a Class
struct Base final { virtual ~Base() = default; };
struct D : Base {};error: base 'Base' is marked 'final'No class may derive from it at all. This is the stronger statement and the more useful one: a concrete leaf class in a hierarchy is usually final, because it was written as an implementation rather than as something to extend further. Chapter 16.8 §8's observation that depth is a symptom applies — final is how you stop depth accumulating by default.
And it has a measurable second effect. Chapter 17.2 §8 measured that knowing the exact static type is not enough to remove the dispatch:
viaBaseRef (const VB&) 3 instr ldr / ldr / br dispatched
viaDerived (const VD&) 3 instr ldr / ldr / br dispatched
viaFinalFn (const VDF&) 2 instr mov w0,#3 / ret devirtualised
viaFinalCls(const VDS&) 2 instr mov w0,#4 / ret devirtualisedWith final, the call collapsed from three instructions to two — and the two are mov and ret, meaning the function was not merely devirtualised but inlined to a constant. final is the promise that lets the compiler conclude there is nothing else the reference could point at.
(What that saving is actually worth — and which half of it matters — is Chapter 17.7's measurement, not this chapter's claim.)
11. The Honest Limits
Two things these keywords do not do, both measured, because knowing the boundary is what stops them being treated as a guarantee.
final does not prevent hiding. It forbids overriding, which is a different operation:
struct Mid : Base { void run(int n) const final { /* ... */ } };
struct Leaf : Mid { void run(unsigned n) const { /* ... */ } }; // different signaturecompile exit: 0 own-file warnings: 1
finhide.cpp:4:27: warning: 'Leaf::run' hides overloaded virtual function [-Woverloaded-virtual]
via Base& : Mid::run(int) final 7
via Mid& : Mid::run(int) final 7
via Leaf : Leaf::run(unsigned) 7It compiled. Leaf could not override the final function, so instead it declared a new one that hides it — and the same object now answers run(7) two different ways depending on which reference you hold. final closed the dispatch door; the hiding door was never locked. The -Woverloaded-virtual warning is the only thing flagging it, and §5 measured a case where that warning does not fire at all.
So final is a statement about dispatch, not about names. Chapter 16.6 §6's using Base::run; discipline still applies inside a hierarchy that uses final.
And override does not check what the function does. It checks a signature:
struct Scaler { virtual int apply(int x) const { return x * 2; } }; // contract: double it
struct Wrong : Scaler { int apply(int x) const override { return x + 2; } };compile exit: 0 own-file warnings: 0
base contract says double; override returned 7 for 5 (doubling gives 10)A perfect override of the wrong behaviour. Zero warnings, correct signature, override present and satisfied — and the function violates the base's contract, which is Chapter 16.3 §1's substitutability test failing. No keyword catches that, for the reason Chapter 16.1's callout gave: the compiler has no representation of what the base promised.
The division is worth holding explicitly. override and final protect the mechanism — that your function is wired to the one you meant. Whether it should be wired there at all is Chapter 16.3's test; whether it behaves correctly once wired is Chapter 17.9's interface design and your own discipline.
12. What to Actually Do
Write override on every derived virtual function, always.
Do not write virtual and override together — the first is implied.
Write override on a derived destructor too; it asserts the base's is virtual.
Mark concrete leaf classes final unless you have a reason to leave them open.
Use final on a function when a behaviour is settled and further overriding would break a dependency.
Do not rely on -Woverloaded-virtual alone — it exists and it is in -Wall, but a warning is not an error.
Check your project really builds with -Wall — measured, -Wextra alone gives nothing here.
Keep using Base::name; in mind even inside a final hierarchy — measured, final stops overriding and not hiding.
And do not read override as a correctness check. It verifies wiring, not behaviour — measured, a perfectly-formed override returned 7 where the base contract said 10.
13. Mistakes Worth Making Once
A one-character signature mismatch. Measured: the base version ran through every base reference; the derived function was unreachable.
Omitting override. You get a warning in -Wall rather than an error — and only if the project enables it.
Assuming -Wextra covers it. Measured: 0 warnings.
Missing const on the derived function. Not an override; caught, but only with the keyword or the warning.
Misspelling the name. only virtual member functions can be marked 'override' — which means nothing matches, not you may not write this.
A different return type. A distinct, clear diagnostic.
Writing virtual and override together. Redundant and mildly confusing.
Leaving every class in a hierarchy open. final on leaves is the default worth adopting.
Expecting an exact static type to be devirtualised. Measured: it is not; final is what does it.
14. Interview Relevance
"What does override do?" Makes it a compile error if the function does not actually override a base virtual. The strong answer notes it has no runtime effect and documents intent.
"What happens without it, if the signature is wrong?" You get a new function that hides the base one, so the base's implementation keeps running through base references. Measured.
"Does the compiler warn?" Yes — -Woverloaded-virtual, and it is in -Wall with a note naming the mismatched parameter. Knowing it is -Wall and not -Wextra is a good detail.
"Then why write override?" A warning is not an error, and the keyword also protects against the base changing later.
"What does final mean on a function? On a class?" No further override; no further derivation.
"Does final affect code generation?" Yes — it permits devirtualisation. Measured, a call collapsed from three instructions to two and inlined to a constant.
"Should every class be final?" Concrete leaves usually, interfaces never.
15. What You Should Be Able to Say Now
- An override must match name, parameter types, constness and ref-qualifier exactly, with an identical or covariant return type.
- Any mismatch declares a new function that hides the base's — Chapter 16.6 §3's rule.
- Measured:
unsignedinstead ofintmeantBase::run(int)ran through aBase&, and the derived function was unreachable polymorphically. - That is worse than non-virtual hiding, because the derived function looks live and is dead.
- Measured, and a genuine exception to this curriculum's pattern: clang warns by default —
-Woverloaded-virtual, with a note naming'int' vs 'unsigned int'. - Measured: it is in
-Wall, not-Wextra—-Wextraalone gave 0 warnings. - It also catches a missing
const. overrideis still worth writing: a warning is not an error, it documents intent, and it protects against base-class changes.- Measured: with
override, the same mistake is an error. - Four distinct diagnostics for wrong parameter type, missing
const, misspelled name, and wrong return type. only virtual member functions can be marked 'override'means nothing matched — not that the keyword is disallowed.overrideon a destructor asserts the base's destructor is virtual — measured refusing when it was not.- Do not write
virtualandoverridetogether. finalon a function forbids further overriding; on a class, further derivation.- Measured:
finalpermitted devirtualisation — 3 instructions down to 2, inlined to a constant. - Mark concrete leaf classes
finaland interfaces never. - Measured limit:
finalforbids overriding but not hiding — a derived class declared a different-signaturerunand the same object answered two ways. - Measured limit:
overridechecks a signature, not behaviour — a correctly-formed override returned 7 where the base contract said 10, with 0 warnings. - So the keywords protect the mechanism; substitutability is Chapter 16.3's test and correctness is Chapter 17.9's design.
16. Exercises
Exercise 1 — the exact match. List everything that must agree for an override, and say what happens if each one does not.
Exercise 2 — predict the behaviour. A base virtual void step(double) and a derived void step(float). Say which runs through a base reference and whether the derived one is ever reachable.
Exercise 3 — the diagnostics. For four deliberate mistakes with override written, give the message you expect for each.
Exercise 4 — the flag. Say which warning catches a signature mismatch without override, which flag set enables it, and which does not.
Exercise 5 — why the keyword. Give three reasons to write override even though the warning exists.
Exercise 6 — the destructor. Explain what ~Derived() override asserts and what error you get if the assertion is false.
Exercise 7 — final on a function. Give a case where marking an override final is the right design, and say what depends on it.
Exercise 8 — final on a class. Say what final changes about the generated code, with the measured instruction counts.
Exercise 9 — audit a hierarchy. For a three-level hierarchy, say where you would put override and final and why.
17. Summary
- An override must match exactly — name, parameters, constness, ref-qualifier, with an identical or covariant return type.
- Measured: one character (
unsignedforint) left the base's implementation running through every base reference, with the derived function unreachable. - Measured, and a real exception to the pattern of this curriculum: clang warns by default —
-Woverloaded-virtualin-Wall(not-Wextra), with a note naming both types. - Write
overrideanyway: a warning is not an error, it states intent, and it protects you when the base changes. - Measured: with
overridethe mistake is a compile error, and four distinct mistakes give four distinct messages. overrideon a destructor asserts the base's is virtual — measured refusing when it was not.finalforbids further overriding (on a function) or derivation (on a class).- Measured:
finalpermitted devirtualisation — 3 instructions to 2, inlined to a constant. - Mark concrete leaves
final; never an interface.
The mechanism dispatches and the signatures are now checked, which leaves the question of what a base class should contain. Every base in this module so far has had working implementations — VBase::run prints something, Base::setup does something harmless — and that makes the base a default rather than a contract. Two consequences follow, and both are on the frozen list of this module's classic failures. A derived class that forgets to implement something silently inherits the base's version, so a missing implementation is not an error but a wrong answer. And the base can be instantiated, which means an object can exist that is supposed to represent "any model" and in fact represents none. The next chapter is the construct that fixes both with two characters, along with the measurements of what it refuses: instantiating the base, deriving without implementing, passing by value, and putting the base in a container — the last two being refusals that turn out to prevent Chapter 17.6's bug by construction.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
