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:

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:

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