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 overridefinal 只在成員函式宣告尾端等特定位置具保留意義,舊程式碼用 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 能一併揪出
contextual keywords

overridefinal 是 C++11 的 contextual keywords(語境關鍵字):只在成員函式宣告尾端才有保留意義。因此舊程式碼裡名為 override 的函式(如 void override();)在 C++98 與 C++11 中都合法且語意不變,升級無痛。

final 的兩種用途

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" 最佳化

理論上編譯器可透過 as if rule 把上述拷貝優化掉,但不可指望編譯器一定找得到這種機會;正確做法是用 reference qualifiers 明確區分兩種情境。

覆寫時 reference qualifier 必須一字不差

若 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 / finalcontextual keywords,僅在特定位置保留
禁止函式再被覆寫/類別被繼承 final(函式尾端/類別名後)
void f() && 的意義 該成員函式只能被 rvalue 的 *this 呼叫
讓臨時物件呼叫 getter 時 move 而非 copy reference qualifiers 多載:& 版回傳 lvalue reference、&& 版回傳 std::move(member)
overriding vs overloading 覆寫=透過 base 介面呼叫 derived 實作;多載=同名不同參數,兩者無關