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


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

6. Rules of Thumb