第16.7章 override 关键字
1. 唯一需要 virtual 的一页
override 是本章唯一离开 virtual 就没有意义的关键字:
它标记一个覆盖基类虚函数的函数,而基类函数不是虚的就没有东西可覆盖。
所以本页只声明一个虚函数、在派生类那个函数上写 override,然后就到此为止。
调用如何被分派 —— vtable、动态类型、为什么 virtual 要花每个对象一个指针 ——
都在第18章。
在机制之前先学这个注解是有好处的。忘记写 override 是 C++ 里最容易
制造、也最难发现的 bug 之一:派生函数能编译,把基类函数隐藏掉,
然后程序一直跑的是基类版本。
2. 规则
- 基类函数声明为
virtual;那是唯一需要这个词的地方。 - 派生类里每个覆盖函数都在声明末尾写
override, 写在const之后、noexcept/throws之前。 - 派生类不重复
virtual。它仍然是虚的 —— 虚性是继承来的 —— 写第二遍毫无收益,只会暗示作者自己也不确定。
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. 经验法则
- 每个覆盖函数都写
override,永远,没有例外。 - 派生类里永远不写
virtual,而是写override。 - 除非你就是设计来被继承的,否则把类标成
final;再覆盖一次就是 bug 时, 把那个虚函数标成final。 override写在const之后、noexcept/throws之前:name() const noexcept override—— 顺序就是本书用的const noexcept override。- 任何通过基类指针删除的代码之前,基类析构函数必须是
virtual(而且仍然是noexcept)。