条款12:使用override关键字声明覆盖的函数
=========================
`C++`中的面向对象的变成都是围绕类,继承和虚函数进行的。其中最基础的一部分就是,派生类中的虚函数会覆盖掉基类中对应的虚函数。但是令人心痛的意识到虚函数重载是如此容易搞错。这部分的语言特性甚至看上去是按照墨菲准则设计的,它不需要被遵从,但是要被膜拜。
因为覆盖“`overriding`”听上去像重载“`overloading`”,但是它们完全没有关系,我们要有一个清晰地认识,虚函数(覆盖的函数)可以通过基类的接口来调用一个派生类的函数:
```cpp
class Base{
public:
virtual void doWork(); // 基类的虚函数
...
};
class Derived: public Base{
public:
virtual void doWork(); // 覆盖 Base::doWork
// ("virtual" 是可选的)
...
};
std::unique_ptr<Base> upb = // 产生一个指向派生类的基类指针
// 关于 std::make_unique 的信息参考条款21
std::make_unique<Derived>();
...
upb->doWork(); // 通过基类指针调用 doWork(),
// 派生类的对应函数别调用
```
如果要使用覆盖的函数,几个条件必须满足:
- 基类中的函数被声明为虚的。
- 基类中和派生出的函数必须是完全一样的(出了虚析构函数)。
- 基类中和派生出的函数的参数类型必须完全一样。
- 基类中和派生出的函数的常量特性必须完全一样。
- 基类中和派生出的函数的返回值类型和异常声明必须使兼容的。
以上的约束仅仅是`C++98`中要求的部分,`C++11`有增加了一条:
- 函数的引用修饰符必须完全一样。成员函数的引用修饰符是很少被提及的`C++11`的特性,所以你之前没有听说过也不要惊奇。这些修饰符使得将这些函数只能被左值或者右值使用成为可能。成员函数不需要声明为虚就可以使用它们:
```cpp
class Widget{
public:
...
void doWork() &; // 只有当 *this 为左值时
// 这个版本的 doWorkd()
// 函数被调用
void doWork() &&; // 只有当 *this 为右值
// 这个版本的 doWork()
// 函数被调用
};
...
Widget makeWidget(); // 工厂函数,返回右值
Widget w; // 正常的对象(左值)
...
w.doWork(); // 为左值调用 Widget::doWork()
//(即 Widget::doWork &)
makeWidget().doWork(); // 为右值调用 Widget::doWork()
//(即 Widget::doWork &&)
```
稍后我们会更多介绍带有引用修饰符的成员函数的情况,但是现在,我们只是简单的提到:如果一个虚函数在基类中有一个引用修饰符,派生类中对应的那个也必须要有完全一样的引用修饰符。如果不完全一样,派生类中的声明的那个函数也会存在,但是它不会覆盖基类中的任何东西。
对覆盖函数的这些要求意味着,一个小的错误会产生一个很大不同的结果。在覆盖函数中出现的错误通常还是合法的,但是它导致的结果并不是你想要的。所以当你犯了某些错误的时候,你并不能依赖于编译器对你的通知。例如,下面的代码是完全合法的,乍一看,看上去也是合理的,但是它不包含任何虚覆盖函数——没有一个派生类的函数绑定到基类的对应函数上。你能找到每种情况里面的问题所在吗?即为什么派生类中的函数没有覆盖基类中同名的函数。
```cpp
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1();
virtual void mf2(unsigned int x);
virtual void mf3() &&;
void mf4() const;
};
```
需要什么帮助吗?
- `mf1`在`Base`中声明常成员函数,但是在`Derived`中没有
- `mf2`在`Base`中以`int`为参数,但是在`Derived`中以`unsigned int`为参数
- `mf3`在`Base`中有左值修饰符,但是在`Derived`中是右值修饰符
- `mf4`没有继承`Base`中的虚函数
你可能会想,“在实际中,这些代码都会触发编译警告,因此我不需要过度忧虑。”也许的确是这样,但是也有可能不是这样。经过我的检查,发现在两个编译器上,上边的代码被全然接受而没有发出任何警告,在这两个编译器上所有警告是都会被输出的。(其他的编译器输出了这些问题的警告信息,但是输出的信息也不全。)
因为声明派生类的覆盖函数是如此重要,有如此容易出错,所以`C++11`给你提供了一种可以显式的声明一个派生类的函数是要覆盖对应的基类的函数的:声明它为`override`。把这个规则应用到上面的代码得到下面样子的派生类:
```cpp
class Derived: public Base {
public:
virtual void mf1() override;
virtual void mf2(unsigned int x) override;
virtual void mf3() && override;
virtual void mf4() const override;
};
```
这当然是无法通过编译的,因为当你用这种方式写代码的时候,编译器会把覆盖函数所有的问题揭露出来。这正是你想要的,所以你应该把所有覆盖函数声明为`override`。
使用`override`,同时又能通过编译的代码如下(假设目的就是`Derived`类中的所有函数都要覆盖`Base`对应的虚函数):
```cpp
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1() const override;
virtual void mf2(int x) override;
virtual void mf3() & override;
void mf4() const override; // 加上"virtual"也可以
// 但是不是必须的
};
```
注意在这个例子中,代码能正常工作的一个基础就是声明`mf4`为`Base`类中的虚函数。绝大部分关于覆盖函数的错误发生在派生类中,但是也有可能在基类中有不正确的代码。
对于派生类中覆盖体都声明为`override`不仅仅可以让编译器在应该要去覆盖基类中函数而没有去覆盖的时候可以警告你。它还可以帮助你预估一下更改基类里的虚函数的标识符可能会引起的后果。如果在派生类中到处使用了`override`,你可以改一下基类中的虚函数的名字,看看这个举动会造成多少损害(即,有多少派生类无法通过编译),然后决定是否可以为了这个改动而承受它带来的问题。如果没有`override`,你会希望此处有一个无所不包的测试单元,因为,正如我们看到的,派生类中那些原本被认为要覆盖基类函数的部分,不会也不需要引发编译器的诊断信息。
- 出版者的忠告
- 致谢
- 简介
- 第一章 类型推导
- 条款1:理解模板类型推导
- 条款2:理解auto类型推导
- 条款3:理解decltype
- 条款4:知道如何查看类型推导
- 第二章 auto关键字
- 条款5:优先使用auto而非显式类型声明
- 条款6:当auto推导出非预期类型时应当使用显式的类型初始化
- 第三章 使用现代C++
- 条款7:创建对象时区分()和{}
- 条款8:优先使用nullptr而不是0或者NULL
- 条款9:优先使用声明别名而不是typedef
- 条款10:优先使用作用域限制的enmu而不是无作用域的enum
- 条款11:优先使用delete关键字删除函数而不是private却又不实现的函数
- 条款12:使用override关键字声明覆盖的函数
- 条款13:优先使用const_iterator而不是iterator
- 条款14:使用noexcept修饰不想抛出异常的函数
- 条款15:尽可能的使用constexpr
- 条款16:保证const成员函数线程安全
- 条款17:理解特殊成员函数的生成
- 第四章 智能指针
- 条款18:使用std::unique_ptr管理独占资源
- 条款19:使用std::shared_ptr管理共享资源
- 条款20:在std::shared_ptr类似指针可以悬挂时使用std::weak_ptr
- 条款21:优先使用std::make_unique和std::make_shared而不是直接使用new
- 条款22:当使用Pimpl的时候在实现文件中定义特殊的成员函数
- 第五章 右值引用、移动语义和完美转发
- 条款23:理解std::move和std::forward
- 条款24:区分通用引用和右值引用
- 条款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 条款26:避免在通用引用上重定义函数
- 条款27:熟悉通用引用上重定义函数的其他选择
- 条款28:理解引用折叠
- 条款29:假定移动操作不存在,不廉价,不使用
- 条款30:熟悉完美转发和失败的情况
- 第六章 Lambda表达式
- 条款31:避免默认的参数捕捉
- 条款32:使用init捕捉来移动对象到闭包
- 条款33:在auto&&参数上使用decltype当std::forward auto&&参数
- 条款34:优先使用lambda而不是std::bind
- 第七章 并发API
- 条款35:优先使用task-based而不是thread-based
- 条款36:当异步是必要的时声明std::launch::async
- 条款37:使得std::thread在所有的路径下无法join
- 条款38:注意线程句柄析构的行为
- 条款39:考虑在一次性事件通信上void的特性
- 条款40:在并发时使用std::atomic 在特殊内存上使用volatile
- 第八章 改进
- 条款41:考虑对拷贝参数按值传递移动廉价,那就尽量拷贝
- 条款42:考虑使用emplace代替insert