Ch16.7: The override Keyword
1. The One Page That Needs virtual
override is the only keyword in this chapter that has no meaning without
virtual: it marks a function that overrides a virtual base-class
function, and if the base function is not virtual there is nothing to override. So this
page declares one virtual function, writes override on the derived one, and
stops there. How the call is dispatched — vtables, dynamic type, why
virtual costs a pointer per object — is
Chapter 18.
There is a good reason to learn the annotation before the mechanism. Forgetting
override is one of the cheapest bugs in C++ to create and one of the most
expensive to find: the derived function compiles, hides the base function, and
the program keeps running the base version.
2. The Rule
- The base function is declared
virtual; that is the only place the keyword is needed. - Every overriding function in a derived class is written with
overrideat the end of its declaration, afterconstand beforenoexcept/throws. - The derived class does not repeat
virtual. It is still virtual — virtuality is inherited — and writing it twice adds nothing but the suggestion that the author was unsure.
struct shape
{
[[nodiscard]] virtual ::std::string_view name() const noexcept { return "shape"; }
[[nodiscard]] ::std::string_view category() const noexcept { return "polygon"; }
virtual ~shape() noexcept = default;
};
struct square : shape
{
[[nodiscard]] ::std::string_view name() const noexcept override { return "square"; }
};
struct unit_square final : square
{
[[nodiscard]] ::std::string_view name() const noexcept override { return "unit square"; }
};
name() is virtual, so a call through a
shape const& reaches the most-derived override;
category() is not virtual, so the same call runs
shape::category() no matter what the object really is. That contrast is
the whole of Chapter 18 in one line.
3. What override Buys: Three Errors Become Compile Errors
override asks the compiler to prove that this function overrides
something. If it does not, the program does not compile. Without the keyword, each of
these three programs compiles silently and misbehaves.
Forgetting the keyword on a non-virtual base function
struct base
{
[[nodiscard]] ::std::string_view name() const noexcept { return "base"; } // not virtual
};
struct derived : base
{
[[nodiscard]] ::std::string_view name() const noexcept override { return "derived"; }
};
error: ‘std::string_view derived::name() const’ marked ‘override’, but does not override
A signature that almost matches
struct base2
{
[[nodiscard]] virtual ::std::string_view name() const noexcept { return "base"; }
virtual ~base2() noexcept = default;
};
struct derived2 : base2
{
[[nodiscard]] ::std::string_view name() noexcept override { return "derived"; } // missing const
};
error: ‘std::string_view derived2::name()’ marked ‘override’, but does not override
The second case is the dangerous one. Drop the const and drop
override, and nobody says anything: derived2 declares a new
function, the base's name() stays in place, and every call through a base
reference keeps printing base.
Deriving from something final
struct base3 final { /* ... */ };
struct derived3 : base3 { /* ... */ };
error: cannot derive from ‘final’ base ‘base3’ in derived type ‘derived3’
final on a class forbids deriving from it; final on a virtual
function forbids overriding it further. Both are documentation the compiler enforces,
and both belong next to override in a well-written hierarchy.
4. The Whole Program
#include <fast_io.h>
#include <string_view>
#include <type_traits>
struct shape
{
[[nodiscard]] virtual ::std::string_view name() const noexcept { return "shape"; }
[[nodiscard]] ::std::string_view category() const noexcept { return "polygon"; }
virtual ~shape() noexcept = default;
};
struct square : shape
{
[[nodiscard]] ::std::string_view name() const noexcept override { return "square"; }
};
struct unit_square final : square
{
[[nodiscard]] ::std::string_view name() const noexcept override { return "unit square"; }
};
int main()
{
try
{
unit_square u{};
shape const &r{u};
::fast_io::io::println("r.name()=", r.name());
::fast_io::io::println("r.category()=", r.category());
::fast_io::io::println("is_polymorphic<shape>=", ::std::is_polymorphic_v<shape>);
::fast_io::io::println("sizeof(shape)=", sizeof(shape), " sizeof(square)=", sizeof(square));
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
r.name()=unit square
r.category()=polygon
is_polymorphic<shape>=1
sizeof(shape)=8 sizeof(square)=8
r.name() printed unit square although r is a
shape const&: that is the virtual call.
r.category() printed polygon for the same object: that is the
non-virtual one. is_polymorphic_v is true because the class
has a virtual function, and sizeof(shape) is 8 because it now
carries a pointer to the vtable.
5. What This Chapter Does Not Do With It
- No owning base pointers.
vector<shape*>of objects that must be deleted through a base pointer needsvirtual ~shape()and careful ownership; Chapter 18 covers both. In this chapter's non-virtual hierarchies the rule was: do not own through a base pointer at all. - No
virtualin constructors or destructors. During construction the object is still a base object; the derived override is not reachable. That is why Ch16.2 says the derived body cannot help build the base. - No dispatch-based design. If the set of types is known at compile
time, a template or a
::std::variantis faster; if it is open, the type erasure of Ch15.10 needs no base class at all.
6. Rules of Thumb
- Write
overrideon every overriding function, always, with no exceptions. - Never write
virtualin a derived class; writeoverrideinstead. - Mark classes
finalunless you designed them to be inherited from, and mark individual virtual functionsfinalwhen a further override would be a bug. - Put
overrideafterconstand beforenoexcept/throws:name() const noexcept override— note the order this book uses,const noexcept override. - A base destructor must be
virtual(and stillnoexcept) before any code deletes through a base pointer.