Item 12:以 override 宣告覆寫函式 (Declare Overriding Functions override)
Overview Table
| 重點 | 說明 |
|---|---|
| 核心建議 | 所有意圖覆寫 (override) base class virtual 函式的 derived class 函式,一律加上 override |
| 問題根源 | 覆寫條件多且嚴格,寫錯時程式仍合法編譯,語意卻悄悄變成「宣告同名新函式」而非覆寫 |
override 的作用 |
要求編譯器檢查「此函式確實覆寫了 base class 的 virtual 函式」,不符即編譯錯誤 |
| 附加好處 | 想改 base class virtual 簽章時,重新編譯即可量化所有受影響的 derived class |
| contextual keywords | override 與 final 只在成員函式宣告尾端等特定位置具保留意義,舊程式碼用 override 當識別字仍合法 |
| reference qualifiers | C++11 新增:成員函式可用 & / && 限定 *this 為 lvalue / rvalue,覆寫時必須完全一致 |
| 易混淆 | overriding(覆寫)≠ overloading(多載),名稱相似但完全無關 |
覆寫成立的六大條件
覆寫讓你能透過 base class 介面呼叫 derived class 的實作:
class Base {
public:
virtual void doWork(); // base class virtual 函式
};
class Derived : public Base {
public:
virtual void doWork(); // 覆寫 Base::doWork
}; // (此處 virtual 可省略)
std::unique_ptr<Base> upb = // base class 指標
std::make_unique<Derived>(); // 指向 derived class 物件
upb->doWork(); // 透過 base 指標呼叫,
// 實際執行 Derived::doWork
upb->doWork();
|
v
+--------------------------+
| 六大覆寫條件全部符合? |
+--------------------------+
| 是 | 否
v v
虛擬分派呼叫 Derived 版本只是不相干的
Derived::doWork 「同名新函式」;透過 Base
(覆寫成立) 介面呼叫到的仍是 Base 版本
| # | 條件 | 出處 |
|---|---|---|
| 1 | base class 函式必須是 virtual(最常被忘記) | C++98 |
| 2 | 函式名稱必須相同(destructor 除外) | C++98 |
| 3 | 參數型別必須完全相同(int vs unsigned int 不算) |
C++98 |
| 4 | constness 必須完全相同 | C++98 |
| 5 | 回傳型別與 exception specification 必須相容 | C++98 |
| 6 | reference qualifier(& / &&)必須完全相同 |
C++11 新增 |
以下程式碼完全合法,卻一個覆寫也沒有:
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const; // 沒宣告 virtual!
};
class Derived : public Base {
public:
virtual void mf1(); // 少了 const → 不覆寫
virtual void mf2(unsigned int x); // int vs unsigned int → 不覆寫
virtual void mf3() &&; // & vs && → 不覆寫
void mf4() const; // Base::mf4 非 virtual → 不覆寫
};
你可能以為「這種錯編譯器會警告」——Meyers 實測兩款編譯器在全部警告開啟下對上述程式碼毫無怨言;其他編譯器也只警告部分問題。沒有 override 時,唯一防線是完整的單元測試。
override 關鍵字:讓編譯器替你把關
在 derived class 中對每個意圖覆寫的函式加上 override,錯誤立即現形(上例四個函式全數編譯失敗)。修正後的正確版本:
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
virtual void mf4() const; // 修正:base 端也要 virtual
};
class Derived : public Base {
public:
virtual void mf1() const override; // OK:簽章完全一致
virtual void mf2(int x) override; // OK
virtual void mf3() & override; // OK
void mf4() const override; // OK:derived 端 virtual 可省略
};
| 好處 | 說明 |
|---|---|
| 即時診斷 | 想覆寫卻沒覆寫成功 → 編譯錯誤,而非默默宣告新函式 |
| 重構影響評估 | 想改 base class virtual 簽章時,改完重新編譯,看多少 derived class 編譯失敗即可量化影響範圍,再決定是否值得;不必祈禱單元測試夠齊全 |
| 雙向把關 | 覆寫錯誤多發生在 derived class,但 base class 也可能出錯(如 mf4 忘了 virtual),override 能一併揪出 |
override 與 final 是 C++11 的 contextual keywords(語境關鍵字):只在成員函式宣告尾端才有保留意義。因此舊程式碼裡名為 override 的函式(如 void override();)在 C++98 與 C++11 中都合法且語意不變,升級無痛。
final 用在 virtual 函式上表示禁止 derived class 再覆寫;用在類別上表示禁止被當作 base class 繼承。兩者都是尾端的 contextual keyword,別與 override 混淆。
成員函式 reference qualifiers:區分 *this 是 lvalue 還是 rvalue
一般函式用 Widget& / Widget&& 參數區分 lvalue / rvalue 引數;reference qualifiers 則對被呼叫物件 *this 做同樣區分——精確類比於尾端 const 表示 *this 為 const。不需是 virtual 函式也能使用:
class Widget {
public:
using DataType = std::vector<double>; // 見 Item 9 的 alias declaration
DataType& data() & // *this 為 lvalue 時:
{ return values; } // 回傳 lvalue reference(供拷貝)
DataType data() && // *this 為 rvalue(臨時物件)時:
{ return std::move(values); } // 回傳暫時物件(move 出資料)
private:
DataType values;
};
Widget makeWidget(); // 工廠函式,回傳 rvalue
Widget w;
auto vals1 = w.data(); // 呼叫 & 版本 → copy 建構 vals1
auto vals2 = makeWidget().data(); // 呼叫 && 版本 → move 建構 vals2
| 呼叫端物件 | 選中的 overload | 回傳型別 | 效果 |
|---|---|---|---|
lvalue(如 w) |
data() & |
DataType&(lvalue) |
copy 建構 |
rvalue(臨時 Widget) |
data() && |
DataType(rvalue) |
move 建構 |
動機:若 data() 一律回傳 lvalue reference,makeWidget().data() 會從即將消滅的暫時物件中複製整個 vector——因為 lvalue reference 是 lvalue,C++ 規則要求編譯器產生拷貝。
理論上編譯器可透過 as if rule 把上述拷貝優化掉,但不可指望編譯器一定找得到這種機會;正確做法是用 reference qualifiers 明確區分兩種情境。
若 base class virtual 函式帶有 reference qualifier,derived class 的覆寫必須有完全相同的 qualifier;不同則函式仍合法存在於 derived class,但不構成覆寫(見六大條件之 6)。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
| derived 函式與 base virtual「長得很像」,透過 base 指標卻呼叫到 base 版本 | 覆寫條件未全部滿足(const/參數型別/reference qualifier/base 非 virtual)→ 一律加 override |
virtual void f() const(base)vs virtual void f()(derived) |
constness 不同 → 不是覆寫,是新函式 |
base void f(int) vs derived void f(unsigned int) |
參數型別不同 → 不是覆寫 |
base void f() & vs derived void f() && |
reference qualifier 不同 → 不是覆寫 |
| 想評估「改 base class virtual 簽章」影響多少子類 | 全面使用 override → 改完重新編譯,編譯錯誤數即影響範圍 |
舊程式有名為 override 的函式,升級 C++11 會壞嗎 |
不會;override / final 是 contextual keywords,僅在特定位置保留 |
| 禁止函式再被覆寫/類別被繼承 | final(函式尾端/類別名後) |
void f() && 的意義 |
該成員函式只能被 rvalue 的 *this 呼叫 |
| 讓臨時物件呼叫 getter 時 move 而非 copy | reference qualifiers 多載:& 版回傳 lvalue reference、&& 版回傳 std::move(member) |
| overriding vs overloading | 覆寫=透過 base 介面呼叫 derived 實作;多載=同名不同參數,兩者無關 |
Related Notes
- 04-Moving-To-Modern-Cpp/05-Deleted-Functions — 同章 Item 11:
= delete與override同為「讓意圖明確、錯誤提前到編譯期」的 C++11 工具 - 04-Moving-To-Modern-Cpp/03-Alias-Declarations — 範例中
using DataType = std::vector<double>的 alias declaration(Item 9) - 04-Moving-To-Modern-Cpp/08-Noexcept — 覆寫條件之一:exception specification 必須相容
- 06-Move-Semantics-And-Perfect-Forwarding/01-Std-Move-And-Std-Forward —
&&qualified 版本以std::move將資料成員轉為 rvalue 的原理 - 01-Introduction/01-Terms-And-Conventions — lvalue/rvalue 基礎術語,是理解 reference qualifiers 的前提
- 05-Smart-Pointers/04-Make-Unique-And-Make-Shared — 範例中以
std::make_unique建立 base 指標指向 derived 物件(Item 21)