第16.7章 override 关键字

English

1. 唯一需要 virtual 的一页

override 是本章唯一离开 virtual 就没有意义的关键字: 它标记一个覆盖基类虚函数的函数,而基类函数不是虚的就没有东西可覆盖。 所以本页只声明一个虚函数、在派生类那个函数上写 override,然后就到此为止。 调用如何被分派 —— vtable、动态类型、为什么 virtual 要花每个对象一个指针 —— 都在第18章。

在机制之前先学这个注解是有好处的。忘记写 override 是 C++ 里最容易 制造、也最难发现的 bug 之一:派生函数能编译,把基类函数隐藏掉, 然后程序一直跑的是基类版本。

2. 规则


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() 是虚的,所以通过 shape const& 的调用会走到最派生的 覆盖版本;category() 不是虚的,所以同一个调用无论对象真实类型是什么, 跑的都是 shape::category()。这个对比就是 第18章的全部内容,一行说完。

3. override 买到了什么:三个错误变成编译错误

override 是在要求编译器证明这个函数确实覆盖了什么。证明不了, 就编译不过。没有这个关键字,下面三个程序都会安安静静编译成功然后行为错误。

在非虚基类函数上写了它


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

签名差一点点


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

第二种才危险。去掉 const、再去掉 override, 编译器一个字都不说:derived2 声明了一个新函数,基类的 name() 原地不动,所有通过基类引用的调用继续打印 base。

从 final 的东西派生


struct base3 final { /* ... */ };
struct derived3 : base3 { /* ... */ };

error: cannot derive from ‘final’ base ‘base3’ in derived type ‘derived3’

final 写在类上就是禁止从它派生;写在虚函数上就是禁止再被覆盖。 两者都是编译器会强制执行的文档,在写得好的层次结构里它们和 override 是邻居。

4. 完整程序


#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);
	}
}

输出:


r.name()=unit square
r.category()=polygon
is_polymorphic<shape>=1
sizeof(shape)=8 sizeof(square)=8

r.name() 打印的是 unit square,尽管 r 是 shape const&:那就是虚调用。r.category() 对同一个对象打印 polygon:那就是非虚调用。is_polymorphic_v 是 true 因为类里有虚函数,而 sizeof(shape) 变成 8,因为它现在带着一个 指向 vtable 的指针。

5. 本章不用它做什么

6. 经验法则