條款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