遮蔽父類方法


在〈繼承共同行為〉中,Roleto_string 被繼承了,然而,你也許會想要 SwordsManMagician 各自的 to_string,可以有類別名稱作為前置,這個需求可以藉由在各自的類別中定義 to_string 來達成。例如:

#include <iostream>
#include <string> 
using namespace std;

class Role {
    string name;   // 角色名稱
    int level;     // 角色等級
    int blood;     // 角色血量

public:
    Role(string name, int level, int blood)
     : name(name), level(level), blood(blood) {}

    string to_string() {
        return "(" + 
            this->name + ", " + 
            std::to_string(this->level) + ", " + 
            std::to_string(this->blood) + 
        ")";
    };
};

class SwordsMan : public Role {
public:
    using Role::Role;

    void fight() {
        cout << "揮劍攻擊" << endl;
    }

    string to_string() {
        return "SwordsMan" + Role::to_string();
    };
};

class Magician : public Role {
public:
    using Role::Role;

    void fight() {
        cout << "魔法攻擊" << endl;
    }

    void cure() {
        cout << "魔法治療" << endl;
    }

    string to_string() {
        return "Magician" + Role::to_string();
    };
};

int main() { 
    SwordsMan swordsMan("Justin", 1, 1000);
    Magician magician("Magician", 1, 800);

    swordsMan.fight();
    magician.fight();
    magician.cure();

    cout << swordsMan.to_string() << endl;
    cout << magician.to_string() << endl;

    return 0;
}

在範例中,to_string 方法取得 Role::to_string 的呼叫結果,並加上各自的前置名稱後傳回,Role::to_string 這樣的呼叫,會隱含地傳入目前的 this,作為 Role::to_string 中的 this

這一次雖然同樣是 swordsMan.to_string()magician.to_string() 呼叫,然而使用了子類各自的定義,父類的 to_string 定義被遮蔽(hide),因此執行結果會是:

揮劍攻擊
魔法攻擊
魔法治療
SwordsMan(Justin, 1, 1000)
Magician(Magician, 1, 800)

在子類別中若要呼叫父類建構式或者是父類方法,在其他語言中,會有 super 之類的關鍵字可以用,然而 C++ 必須使用父類名稱,在簡單的情境中,寫死父類名稱或許不是什麼問題,然而,在更複雜的情況,多個方法都得呼叫父類方法時,寫死一大堆父類名稱,可能就是個問題,如果父類名稱在撰寫時又比較複雜,問題可能就更大。

一個緩解的方式是以 using 定義別名。例如:

class SwordsMan : public Role {
    using super = Role;

public:
    using Role::Role;

    void fight() {
        cout << "揮劍攻擊" << endl;
    }

    string to_string() {
        return "SwordsMan" + super::to_string();
    };
};

這麼一來,未來若真的要修改父類名稱,可以只在一個地方修改。

實際上就以上的需求,你也可以在 SwordsManMagician 中定義一個 desc 來完成相同的任務,那麼以相同名稱遮敝父類方法的意義何在呢?

就這邊來說,在還沒遮敝同名方法方法前,swordsMan.to_string()magician.to_string() 在編譯時期,就綁定了呼叫的方法會是 Role 中定義的 to_string 方法,在遮敝同名方法之後,編譯時綁定的版本,就是各自類別中定義的 to_string 方法。

也就是就這邊的範例來說,遮敝同名方法之目的,是要在編譯時期,視實例的型態來綁定對應的方法版本。

如果遮敝了 to_string,然後你這麼呼叫呢?

SwordsMan swordsMan("Justin", 1, 1000);

Role &role = swordsMan;
cout << role.to_string() << endl; // 顯示 (Justin, 1, 1000)

首先,因為繼承會有 is-a 的關係,也就是 SwordsMan 是一種 Role,當 = 左邊型態是一種右邊型態時,編譯器允許隱含的型態轉換,因此 Role role = swordsMan 可以通過編譯。

接下來 role.to_string() 呼叫時,由於編譯器在編譯時期只知道 role 的型態是 Role,雖然 role 實際上參考了 swordsMan,然而編譯時期能綁定的就是 Roleto_string 定義,因此執行的結果會是來自 Roleto_string 定義,而不是 SwordsManto_string 定義。

如果想在編譯時期,不管實例實際上是哪種型態,一律視呼叫方法時的變數型態來決定呼叫的版本,這個行為就會是你要的,在這種情況下,若有個函式或方法,想要操作實例繼承而來,或者是本身定義的方法,會透過模版來達成。例如:

template <typename T>
void printInfo(T &t) {
    cout << t.to_string() << endl;
}

因為是模版,實際上會依呼叫時指定的實例型態,重載出對應型態的版本,該呼叫哪個版本,是編譯時期就決定的事,就程式碼本身而言,是以父類定義的行為來看待實例的操作,是一種多型(polymorphism)的實現,由於這種多型實現是編譯時期就可以達成方法綁定,亦被稱為編譯時期多型(compile-time polymorphism)。

當然,printInfo 模版可以適用任何具有 to_string 方法的實例,不用是 Role 或其子類別的實例,因而可用來實現結構型態系統(structural type system)。

如果想在執行時期,看看實際上實例是何種型態,並採用各自型態的 to_string 定義,該方法必須設定為 virtual,這之後再來說明了。