final/override控制

原文鏈接:http://book.2cto.com/201306/25355.html


在瞭解C++11中的final/override關鍵字之前,我們先回顧一下C++關於重載的概念。簡單地說,一個類A中聲明的虛函數fun在其派生類B中再次被定義,且B中的函數fun跟A中fun的原型一樣(函數名、參數列表等一樣),那麼我們就稱B重載(overload)了A的fun函數。對於任何B類型的變量,調用成員函數fun都是調用了B重載的版本。而如果同時有A的派生類C,卻並沒有重載A的fun函數,那麼調用成員函數fun則會調用A中的版本。這在C++中就實現多態。

在通常情況下,一旦在基類A中的成員函數fun被聲明爲virtual的,那麼對於其派生類B而言,fun總是能夠被重載的(除非被重寫了)。有的時候我們並不想fun在B類型派生類中被重載,那麼,C++98沒有方法對此進行限制。我們看看下面這個具體的例子,如代碼清單2-23所示。

代碼清單2-23
#include <iostream>
using namespace std;

class MathObject{
public:
    virtual double Arith() = 0;
    virtual void Print() = 0;
};

class Printable : public MathObject{
public:
   double Arith() = 0;
   void Print() // 在C++98中我們無法阻止該接口被重寫
   {
       cout << "Output is: " << Arith() << endl;
   }
};

class Add2 : public Printable {
public:
    Add2(double a, double b): x(a), y(b) {}
    double Arith() { return x + y; }
private:
    double x, y;
};

class Mul3 : public Printable {
public:
    Mul3(double a, double b, double c): x(a), y(b), z(c) {}
    double Arith() { return x * y * z; }
private:
    double x, y, z;
};
// 編譯選項:g++ 2-10-1.cpp

在代碼清單2-23中,我們的基礎類MathObject定義了兩個接口:Arith和Print。類Printable則繼承於MathObject並實現了Print接口。接下來,Add2和Mul3爲了使用MathObject的接口和Printable的Print的實現,於是都繼承了Printable。這樣的類派生結構,在面向對象的編程中非常典型。不過倘若這裏的Printable和Add2是由兩個程序員完成的,Printable的編寫者不禁會有一些憂慮,如果Add2的編寫者重載了Print函數,那麼他所期望的統一風格的打印方式將不復存在。

對於Java這種所有類型派生於單一元類型(Object)的語言來說,這種問題早就出現了。因此Java語言使用了final關鍵字來阻止函數繼續重寫。final關鍵字的作用是使派生類不可覆蓋它所修飾的虛函數。C++11也採用了類似的做法,如代碼清單2-24所示的例子。

代碼清單2-24
struct Object{
    virtual void fun() = 0;
};

struct Base : public Object {
    void fun() final;   // 聲明爲final
};

struct Derived : public Base {
    void fun();     // 無法通過編譯
};
// 編譯選項:g++ -c -std=c++11 2-10-2.cpp

在代碼清單2-24中,派生於Object的Base類重載了Object的fun接口,並將本類中的fun函數聲明爲final的。那麼派生於Base的Derived類對接口fun的重載則會導致編譯時的錯誤。同理,在代碼清單2-23中,Printable的編寫者如果要阻止派生類重載Print函數,只需要在定義時使用final進行修飾就可以了。

讀者可能注意到了,在代碼清單2-23及代碼清單2-24兩個例子當中,final關鍵字都是用於描述一個派生類的。那麼基類中的虛函數是否可以使用final關鍵字呢?答案是肯定的,不過這樣將使用該虛函數無法被重載,也就失去了虛函數的意義。如果不想成員函數被重載,程序員可以直接將該成員函數定義爲非虛的。而final通常只在繼承關係的“中途”終止派生類的重載中有意義。從接口派生的角度而言,final可以在派生過程中任意地阻止一個接口的可重載性,這就給面向對象的程序員帶來了更大的控制力。

在C++中重載還有一個特點,就是對於基類聲明爲virtual的函數,之後的重載版本都不需要再聲明該重載函數爲virtual。即使在派生類中聲明瞭virtual,該關鍵字也是編譯器可以忽略的。這帶來了一些書寫上的便利,卻帶來了一些閱讀上的困難。比如代碼清單2-23中的Printable的Print函數,程序員無法從Printable的定義中看出Print是一個虛函數還是非虛函數。另外一點就是,在C++中有的虛函數會“跨層”,沒有在父類中聲明的接口有可能是祖先的虛函數接口。比如在代碼清單2-23中,如果Printable不聲明Arith函數,其接口在Add2和Mul3中依然是可重載的,這同樣是在父類中無法讀到的信息。這樣一來,如果類的繼承結構比較長(不斷地派生)或者比較複雜(比如偶爾多重繼承),派生類的編寫者會遇到信息分散、難以閱讀的問題(雖然有時候編輯器會進行提示,不過編輯器不是總是那麼有效)。而自己是否在重載一個接口,以及自己重載的接口的名字是否有拼寫錯誤等,都非常不容易檢查。

在C++11中爲了幫助程序員寫繼承結構複雜的類型,引入了虛函數描述符override,如果派生類在虛函數聲明時使用了override描述符,那麼該函數必須重載其基類中的同名函數,否則代碼將無法通過編譯。我們來看一下如代碼清單2-25所示的這個簡單的例子。

代碼清單2-25
struct Base {
    virtual void Turing() = 0;
    virtual void Dijkstra() = 0;
    virtual void VNeumann(int g) = 0;
    virtual void DKnuth() const;
    void Print();
};

struct DerivedMid: public Base {
    // void VNeumann(double g);
    // 接口被隔離了,曾想多一個版本的VNeumann函數
};

struct DerivedTop : public DerivedMid {
    void Turing() override;
    void Dikjstra() override;           // 無法通過編譯,拼寫錯誤,並非重載
    void VNeumann(double g) override;   // 無法通過編譯,參數不一致,並非重載
    void DKnuth() override;             // 無法通過編譯,常量性不一致,並非重載
    void Print() override;              // 無法通過編譯,非虛函數重載
};
// 編譯選項:g++ -c -std=c++11 2-10-3.cpp

在代碼清單2-25中,我們在基類Base中定義了一些virtual的函數(接口)以及一個非virtual的函數Print。其派生類DerivedMid中,基類的Base的接口都沒有重載,不過通過註釋可以發現,DerivedMid的作者曾經想要重載出一個“void VNeumann(double g)”的版本。這行註釋顯然迷惑了編寫DerivedTop的程序員,所以DerivedTop的作者在重載所有Base類的接口的時候,犯下了3種不同的錯誤:

函數名拼寫錯,Dijkstra誤寫作了Dikjstra。

函數原型不匹配,VNeumann函數的參數類型誤做了double類型,而DKnuth的常量性在派生類中被取消了。

重寫了非虛函數Print。

如果沒有override修飾符,DerivedTop的作者可能在編譯後都沒有意識到自己犯了這麼多錯誤。因爲編譯器對以上3種錯誤不會有任何的警示。這裏override修飾符則可以保證編譯器輔助地做一些檢查。我們可以看到,在代碼清單2-25中,DerivedTop作者的4處錯誤都無法通過編譯。

此外,值得指出的是,在C++中,如果一個派生類的編寫者自認爲新寫了一個接口,而實際上卻重載了一個底層的接口(一些簡單的名字如get、set、print就容易出現這樣的狀況),出現這種情況編譯器還是愛莫能助的。不過這樣無意中的重載一般不會帶來太大的問題,因爲派生類的變量如果調用了該接口,除了可能存在的一些虛函數開銷外,仍然會執行派生類的版本。因此編譯器也就沒有必要提供檢查“非重載”的狀況。而檢查“一定重載”的override關鍵字,對程序員的實際應用則會更有意義。

還有值得注意的是,如我們在第1章中提到的,final/override也可以定義爲正常變量名,只有在其出現在函數後時纔是能夠控制繼承/派生的關鍵字。通過這樣的設計,很多含有final/override變量或者函數名的C++98代碼就能夠被C++編譯器編譯通過了。但出於安全考慮,建議讀者在C++11代碼中應該儘可能地避免這樣的變量名稱或將其定義在宏中,以防發生不必要的錯誤。http://book.2cto.com/201306/25355.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章