Ch18.3: Mark Every Virtual Function
1. The Rule
If you write a virtual function, in a codebase that follows this book, it must satisfy both of these:
- It is marked — one of
virtual,override, orfinal, always, on every declaration and every definition. No exceptions, not even for a function you just made virtual five seconds ago. - It has an error contract — exactly one of
noexceptorthrows, like every other function (Ch6.16).
Which marker goes where is not a style choice:
| Position | Marker | Meaning, mechanically |
|---|---|---|
| first declaration, in the base | virtual |
"this slot exists in the table" — this is the only place the word belongs |
| a function that overrides | override |
"I claim to override something; error if I do not" |
| a function or class that closes the hierarchy | final |
"no further override exists; calls to this may be devirtualised" |
Never write virtual in a derived class. It is legal, it is redundant, and it
actively hides the mistake you are most likely making: writing virtual on a
function that does not override anything because the signature is slightly
wrong.
2. What Happens Without override
The base has who() const. The derived class forgets const.
The compiler accepts this silently and creates a brand-new function, unrelated to the
virtual one:
struct base
{
virtual ::std::string_view who() const noexcept { return "base"; }
virtual int rank() const noexcept { return 1; }
virtual ~base() noexcept = default;
};
struct forgetful : base
{
// missing const: a brand-new function, not an override
::std::string_view who() noexcept { return "forgetful"; }
};
struct marked : base
{
::std::string_view who() const noexcept override { return "marked"; }
};
forgetful direct=forgetful through base=base | marked direct=marked through base=marked
forgetful answers "forgetful" when you call it on a
forgetful and "base" when you call it through a
base&. Both are the same object. The program is well-formed, and
without -Wall there is not even a warning; with
-Wall -Wextra you get one, and it is easy to misread:
mark.cc:6:36: warning: ‘virtual std::string_view base::who() const’ was hidden [-Woverloaded-virtual=]
Add the missing const and override, and the same call
dispatches the way you meant: marked direct=marked through base=marked.
With override written, a wrong signature is a compile error instead of a
silent third function. That is the entire value of the keyword: it converts your
intent into a checked claim.
3. The Contract Is Checked, the Marker Is Not
The good news: the error contract is enforced by the compiler, in the direction that matters. If the base promises not to fail and the derived class does not, you get an error:
struct base
{
virtual int rank() const noexcept { return 1; }
virtual ~base() noexcept = default;
};
struct child : base
{
int rank() const override { return 2; }
};
mark2.cc:9:13: error: looser exception specification on overriding virtual function ‘virtual int child::rank() const’
9 | int rank() const override { return 2; }
| ^~~~
mark2.cc:3:21: note: overridden function is ‘virtual int base::rank() const noexcept’
3 | virtual int rank() const noexcept { return 1; }
| ^~~~
The reverse direction is allowed and is what you want: a throws virtual may
be overridden by a noexcept one, because narrowing the failure set cannot
break a caller. This is why the contract belongs on the base: the base decides
the weakest contract every implementation must honour, and callers only need to
look at the base.
Write the contracts on the pure virtuals too — virtual double area() const
noexcept = 0; — they are the ones the callers actually read.
4. final: More Than Documentation
final appears in two places, with two different payoffs:
- On a function: no further derived class may override it. The compiler can then call it directly whenever it can prove the static type, because no derived implementation can exist.
- On a class: nothing may derive from it. This lets the compiler skip
the vptr entirely when the object is local and never escapes — measured in this
book's toolchain: with
finaland a local object, the vtable is not emitted at all at-O2, and a subsequentdynamic_cast/link step loses the typeinfo it no longer needs.
final is the only marker with a codegen effect. It is also the only one that
makes a hierarchy safe to close: once a class is final, adding a derived
class later becomes a compile error, which is usually what you wanted to know.
5. Where the Marker Goes, Precisely
struct shape
{
virtual double area() const noexcept = 0; // base: virtual
virtual ::std::string_view name() const noexcept = 0;
virtual ~shape() noexcept = default; // virtual, noexcept
};
struct square final : shape // final class
{
double side{};
explicit square(double s) noexcept : side{s} {}
double area() const noexcept override { return side * side; } // const, then override, then noexcept
::std::string_view name() const noexcept override { return "square"; }
~square() noexcept override = default; // a destructor can override too
};
The order in the declaration is const → override →
noexcept/throws, the same order used throughout this book
(Ch16.7). Putting override
somewhere else is usually a syntax error, and always a reading speed problem.
6. Enforcing It Without a Compiler Flag
There is no -Wrequire-override in this toolchain, and there is no flag that
demands a contract on a virtual function (measured: -Wall -Wextra stays
silent on a virtual function with no specification). So the rule is a project rule, and
project rules need a grep that fails the build:
# every 'virtual' must also be = 0, noexcept or throws
grep -rn 'virtual ' include/ \
| grep -v '= 0' | grep -v 'noexcept' | grep -v 'throws'
# nothing outside a base declaration may say 'virtual'
grep -rn 'virtual ' include/ | grep -v 'struct \|class '
Run those two lines in CI. They catch more real defects than the compiler does, because the compiler is happy with all of the mistakes this chapter is about.
7. Rules of Thumb
- Every virtual function carries
virtual,overrideorfinal. Every one. In a diff review, an unmarked virtual is a rejected patch. - Every virtual function carries
noexceptorthrows. The base decides; the derived may narrow. - Never repeat
virtualin a derived class. Writeoverride, and let the compiler check your signature. - Mark leaf classes
final. It is free, it is checked, and it is the only marker that removes code. - If a class is
finaland the objects stay local, prefer deleting thevirtualaltogether and using an overload set or::std::variant— then there is nothing to mark.