虛擬繼承


類別若繼承兩個以上的抽象類別,而兩個抽象類別都定義了相同方法,那麼子類別會怎樣嗎?程式面上來說,並不會有錯誤,照樣通過編譯:

class Task {
public:
    virtual void execute() = 0;
    virtual void doSome() = 0;
    virtual ~Task() = default;
};

class Command {
public:
    virtual void execute() = 0;
    virtual void doOther() = 0;
    virtual ~Command() = default;
};

class Service : public Task, public Command {
public:
    void execute() override {
        cout << "foo" << endl;
    }

    void doSome() override {
        cout << "some" << endl;
    }

    void doOther() override {
        cout << "other" << endl;
    }
};

但在設計上,你要思考一下:TaskCommand 定義的 execute 是否表示不同的行為?

如果表示不同的行為,那麼 Service 在實作時,應該會有不同的方法實作,那麼 TaskCommandexecute 方法就得在名稱上有所不同,Service 在實作時才可以有兩個不同的方法實作。

如果表示相同的行為,那可以定義一個父類別,在當中定義純虛擬 execute 方法,而 TaskCommand 繼承該類別,各自定義純虛擬的 doSomedoOther 方法:

#include <iostream>
using namespace std;

class Action {
public:
    virtual void execute() = 0;
    virtual ~Action() = default;
};

class Task : public Action {
public:
    virtual void doSome() = 0;
};

class Command : public Action {
public:
    virtual void doOther() = 0;
};

class Service : public Task, public Command {
public:
    void execute() override {
        cout << "service" << endl;
    }

    void doSome() override {
        cout << "some" << endl;
    }

    void doOther() override {
        cout << "other" << endl;
    }
};

int main() { 
    Service service;
    service.execute();
    service.doSome();
    service.doOther();

    Task &task = service;
    task.doSome();

    Command &command = service;
    command.doOther();

    return 0;
}

這個程式可以編譯成功也可以執行,不過從〈多重繼承的建構〉可以知道,taskcommand 的位址會是不同,建構 service 的過程中,TaskCommand 的建構式中 this 會是不同位址,而它們又會以各自的 this 來執行 Action 的建構式。

也就是就上例來說,Action 的建構流程會跑兩次,一次是以 task 的位址,一次是以 command 的位址,這意謂著,如果 Action 定義了值域,taskcommand 會各自擁有一份。

另外要知道的是,目前為止的繼承方式,都是編譯時期就決定了子類從父類繼承而來的定義,例如,單看 Task,編譯時期就決定了從 Action 繼承而來的定義,而單看 Command,編譯時期就決定了從 Action 繼承而來的定義。

結果就是,由於 TaskCommand 各自有一份編譯時期繼承而來的 Action 定義,如果 Service 同時繼承了 TaskCommand,那它會有兩份 Action 定義,各來自 TaskCommand,藉由 this 的實際位址來決定該使用哪個定義。

這就有了個問題,如果是用 Action 型態來參考 service 呢?

Action &action = service; // error: 'Action' is an ambiguous base of 'Service'

由於 Service 有兩份 Action 定義,作為父型態的 Action 要參考 service 時,編譯器不知道你想採用哪份 Action 定義,如果想在編譯時期就決定這件事,就得明確告訴編譯器:

Action &action1 = static_cast<Task&>(service);
Action &action2 = static_cast<Command&>(service);

action1.execute();
action2.execute();

如果不想使用 static_cast 呢?根源在於 TaskCommand 在編譯時期就決定了從 Action 繼承而來的定義,才造成 Service 中有兩份 Action 定義,那能不能在執行時期才決定 TaskCommand 繼承的定義,就類似 virtual 函式,執行時期才決定實際的函式位址?

這可以透過虛繼承,也就是在繼承時加上 virtual 關鍵字來達到:

class Task : public virtual Action {
public:
    virtual void doSome() = 0;
};

class Command : public virtual Action {
public:
    virtual void doOther() = 0;
};

class Service : public Task, public Command {
public:
    void execute() override {
        cout << "service" << endl;
    }

    void doSome() override {
        cout << "some" << endl;
    }

    void doOther() override {
        cout << "other" << endl;
    }
};

現在 TaskCommand 編譯過後,不會各自包含 Action 的定義了,只會各自有個可用來指向 Action 的指標,在執行時期才指向同一個 Action 類別,因此 Service 繼承而來的 Action 類別也就是 TaskCommand 共享的那一個,因此 Action 型態就可以直接參考 Service 實例了:

Action &action = service;
action.execute();

在虛繼承下,Action 的建構式只會以 Service 實例的位址執行一次。

當然,這些都是編譯器的細節,若要從語義上理解,實際上 Service 才真的實作 executeTaskCommand 不用真的包含 Action 定義,virtual 繼承時,TaskCommand 就像是轉接 ActionService 發現這兩個類別轉接的對象是同一個 Action,最後就會像是 Service 直接繼承 Action,若要做個比喻,就會像 class Service : public Action, public Task, public Command

另一種語義上的理解方式是,虛繼承的 TaskCommand 表明,若以 Action 型態參考實例來操作時,TaskCommandthis 願意共用相同的位址,而這個位址會是同時繼承了 TaskCommand 的子類位址,也就是 Service 實例的位址。