Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Base    { virtual ~Base() = default; virtual void run(int n) const { /* ... */ } };
    struct Derived : Base { void run(unsigned n) const { /* ... */ } };   // unsigned, not int

One character. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
through a Base&: Base::run(int) 7
through Derived: Derived::run(unsigned) 7

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Derived : Base { void run(unsigned) const override { } };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: non-virtual member function marked 'override' hides virtual member function

An error, not a warning. The build stops.

7. The Four Mistakes It Catches

Each with override written, and each producing a different message:

Derived declarationDiagnostic
void run(unsigned) const overridenon-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 overridecompiles

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct BadDerived : BadBase { ~BadDerived() override { /* ... */ } };   // base dtor not virtual
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void run(int) const override;              // preferred
    virtual void run(int) const override;      // redundant; `override` implies it

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Mid : Base { void run() const final { } };
    struct Leaf : Mid { void run() const override { } };   // refused
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: declaration of 'run' overrides a 'final' function

Two 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Base final { virtual ~Base() = default; };
    struct D : Base {};
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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     devirtualised

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Mid  : Base { void run(int n) const final { /* ... */ } };
    struct Leaf : Mid  { void run(unsigned n) const { /* ... */ } };   // different signature
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compile 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) 7

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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; } };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

  1. An override must match name, parameter types, constness and ref-qualifier exactly, with an identical or covariant return type.
  2. Any mismatch declares a new function that hides the base's — Chapter 16.6 §3's rule.
  3. Measured: unsigned instead of int meant Base::run(int) ran through a Base&, and the derived function was unreachable polymorphically.
  4. That is worse than non-virtual hiding, because the derived function looks live and is dead.
  5. Measured, and a genuine exception to this curriculum's pattern: clang warns by default — -Woverloaded-virtual, with a note naming 'int' vs 'unsigned int'.
  6. Measured: it is in -Wall, not -Wextra — -Wextra alone gave 0 warnings.
  7. It also catches a missing const.
  8. override is still worth writing: a warning is not an error, it documents intent, and it protects against base-class changes.
  9. Measured: with override, the same mistake is an error.
  10. Four distinct diagnostics for wrong parameter type, missing const, misspelled name, and wrong return type.
  11. only virtual member functions can be marked 'override' means nothing matched — not that the keyword is disallowed.
  12. override on a destructor asserts the base's destructor is virtual — measured refusing when it was not.
  13. Do not write virtual and override together.
  14. final on a function forbids further overriding; on a class, further derivation.
  15. Measured: final permitted devirtualisation — 3 instructions down to 2, inlined to a constant.
  16. Mark concrete leaf classes final and interfaces never.
  17. Measured limit: final forbids overriding but not hiding — a derived class declared a different-signature run and the same object answered two ways.
  18. Measured limit: override checks a signature, not behaviour — a correctly-formed override returned 7 where the base contract said 10, with 0 warnings.
  19. 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 (unsigned for int) 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-virtual in -Wall (not -Wextra), with a note naming both types.
  • Write override anyway: a warning is not an error, it states intent, and it protects you when the base changes.
  • Measured: with override the mistake is a compile error, and four distinct mistakes give four distinct messages.
  • override on a destructor asserts the base's is virtual — measured refusing when it was not.
  • final forbids further overriding (on a function) or derivation (on a class).
  • Measured: final permitted 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.