比較安全的 C++ 虛擬函式寫法:C++11 override 與 final

| | 0 Comments| 09:06
Categories:

很久之前,Heresy 有介紹過一些 C 11(當時還是 C 0x)的新語法,而之後也有陸續寫過一些新的 STL 的介紹(參考《C 11 / Boost C Libraries 目錄》)。

而雖然隔了很久,不過現在還是再回過頭來、記錄一下最近在看的、C 11 裡面和 class 的繼承有關的新語法吧~這次紀錄的,是用來讓類別繼承時的虛擬函式撰寫更安全一點的「override」這個新個語法。

首先,一般在寫繼承的時候,大部分會是像下面的寫法:

class CA
{
public:
    void func1()
    {
        std::cout << "func1 in CA" << std::endl;
    }

    virtual void func2()
    {
        std::cout << "func2 in CA" << std::endl;
    }
};

class CB : public CA
{
public:
    void func1()
    {
        std::cout << "func1 in CB" << std::endl;
    }

    void func2()
    {
        std::cout << "func2 in CB" << std::endl;
    }
};

在上面的例子裡面,CB 這個類別繼承自 CA 這個基礎類別,而由於 CAfunc2() 有加上 virtual,所以會變成一個虛擬函式,也因此在呼叫 CAfunc2() 的時候,他會去確認是要執行物件本身的型別,如果是衍生類別的話、是否有重新實作這個虛擬函式。

這類的程式,通常使用的狀況會是類似下面這樣:

CA* pObjA = new CA();
CB* pObjB = new CB();

pObjA->func1();        // func1 in CA
pObjA->func2();        // func2 in CA
pObjB->func1();        // func1 in CB
pObjB->func2();        // func2 in CB

CA* pObjB2 = pObjB;
pObjB2->func1();        // func1 in CA
pObjB2->func2();        // func2 in CB

這樣的好處,基本上就是可以透過一個共通的介面(CA),來操作不同類型的物件。

但是,在撰寫虛擬函式的時候,其實常常會碰到幾個問題,導致程式出錯;其中最主要的問題,就是

「以為自己是在重新實作基礎類別的虛擬函式、但是實際上沒有」!

這種問題發生的原因有很多種,包括了:

  • 基礎類別的函式忘了加上 virtual(例如上例中的 func1()
  • 虛擬函式的介面不一致
  • 某個類別的虛擬函式的介面修改後,忘了修改其他類別中的函式

下面就是兩個可能會發生的狀況:

class CA
{
public:
    virtual void func1(int)
    {
        std::cout << "func1 in CA" << std::endl;
    }
};

class CB : public CA
{
public:
    void func1(int) const
    {
        std::cout << "func1 in CB" << std::endl;
    }
};

class CC : public CA
{
public:
    void func1(float)
    {
        std::cout << "func1 in CC" << std::endl;
    }
};

CBfunc1() 來說,雖然看來和 CA 的一樣,但是由於他有加上 const,所以還是不同;而 CCfunc1() 則是由於參數型別不同,所以也不會被視為同一個虛擬函式。

所以,在執行下面的程式的時候,就都只會呼叫到 CA::func1(),而不會呼叫到各自的實作了。

CA* pObjA = new CA();
CA* pObjB = new CB();
CA* pObjC = new CC();

pObjA->func1(0);        // func1 in CA
pObjB->func1(0);        // func1 in CA
pObjC->func1(0);        // func1 in CA

而為了避免這類的問題發生,C 11 提供了一個新的語法:「override」,來在編譯階段就可以確定衍生類別的函式的覆寫是否有成功。

它的使用方法也很簡單,只要在衍生類別裡面、要覆寫函式後面加上「override」、告訴編譯器這個函式是要用來覆寫基礎類別的虛擬函式就可以了。而如果加上了「override」,編譯器就會在編譯階段去檢查他是否有真的覆寫到基礎類別的虛擬函式。

如果把上面的例子都加上「override」的話,就變成:

class CA
{
public:
    virtual void func1(int)
    {
        std::cout << "func1 in CA" << std::endl;
    }
};

class CB : public CA
{
public:
    void func1(int) const override
    {
        std::cout << "func1 in CB" << std::endl;
    }
};

class CC : public CA
{
public:
    void func1(float) override
    {
        std::cout << "func1 in CC" << std::endl;
    }
};

而這樣的話,由於 CBCCfunc1() 都沒辦法成功地覆寫到基礎類別的虛擬函式,所以在編譯的時候,就會直接出現編譯錯誤、而不會等到執行階段才發現有問題了!

由於是編譯階段就會發現的錯誤,所以會讓問題及早被發現;如果專案寫得很大、繼承的類別很多的話,就算需要改動基礎類別的虛擬函式,只要衍生類別都有乖乖加上「override」的話,也就可以很方便地找到所有要做對應修改的地方了!


而除了 override 外,C 11 另外也還有一個指示字「finial」,可以用來避免類別被繼承、或是虛擬函式被複寫。

在類別的繼承上,他的用法是:

class CA final
{
};

class CB : public CA
{
};

在上面的例子裡面,因為 CA 這個類別有加上「finial」這個設定,所以它是不能被當成基礎類別而被繼承的;因此,在上面的程式裡面,CB 試圖去繼承 CA 的寫法,是會造成編譯階段的錯誤的。

而如果用在虛擬函式上的話,則會像下面這樣:

class CA
{
public:
    virtual void func1() final;
};

class CB : public CA
{
public:
    void func1() override;
};

在這邊,由於 CAfunc1() 有加上「finial」,編譯器會知道這個虛擬函式不能被衍生類別的函式覆寫;所以當 CB 試著去覆寫的時候,編譯器也會在編譯階段就產生錯誤。

透過這樣的機制,基本上應該是可以更好地去控制、也可以更明確地表達那些函式是要被覆寫、那些函式不可以被覆寫的。


參考:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *