類別若繼承兩個以上的抽象類別,而兩個抽象類別都定義了相同方法,那麼子類別會怎樣嗎?程式面上來說,並不會有錯誤,照樣通過編譯:
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;
}
};
但在設計上,你要思考一下:Task 與 Command 定義的 execute 是否表示不同的行為?
如果表示不同的行為,那麼 Service 在實作時,應該會有不同的方法實作,那麼 Task 與 Command 的 execute 方法就得在名稱上有所不同,Service 在實作時才可以有兩個不同的方法實作。
如果表示相同的行為,那可以定義一個父類別,在當中定義純虛擬 execute 方法,而 Task 與 Command 繼承該類別,各自定義純虛擬的 doSome 與 doOther 方法:
#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;
}
這個程式可以編譯成功也可以執行,不過從〈多重繼承的建構〉可以知道,task 與 command 的位址會是不同,建構 service 的過程中,Task、Command 的建構式中 this 會是不同位址,而它們又會以各自的 this 來執行 Action 的建構式。
也就是就上例來說,Action 的建構流程會跑兩次,一次是以 task 的位址,一次是以 command 的位址,這意謂著,如果 Action 定義了值域,task 與 command 會各自擁有一份。
另外要知道的是,目前為止的繼承方式,都是編譯時期就決定了子類從父類繼承而來的定義,例如,單看 Task,編譯時期就決定了從 Action 繼承而來的定義,而單看 Command,編譯時期就決定了從 Action 繼承而來的定義。
結果就是,由於 Task、Command 各自有一份編譯時期繼承而來的 Action 定義,如果 Service 同時繼承了 Task、Command,那它會有兩份 Action 定義,各來自 Task、Command,藉由 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 呢?根源在於 Task、Command 在編譯時期就決定了從 Action 繼承而來的定義,才造成 Service 中有兩份 Action 定義,那能不能在執行時期才決定 Task、Command 繼承的定義,就類似 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;
}
};
現在 Task、Command 編譯過後,不會各自包含 Action 的定義了,只會各自有個可用來指向 Action 的指標,在執行時期才指向同一個 Action 類別,因此 Service 繼承而來的 Action 類別也就是 Task、Command 共享的那一個,因此 Action 型態就可以直接參考 Service 實例了:
Action &action = service;
action.execute();
在虛繼承下,Action 的建構式只會以 Service 實例的位址執行一次。
當然,這些都是編譯器的細節,若要從語義上理解,實際上 Service 才真的實作 execute,Task、Command 不用真的包含 Action 定義,virtual 繼承時,Task、Command 就像是轉接 Action,Service 發現這兩個類別轉接的對象是同一個 Action,最後就會像是 Service 直接繼承 Action,若要做個比喻,就會像 class Service : public Action, public Task, public Command。
另一種語義上的理解方式是,虛繼承的 Task、Command 表明,若以 Action 型態參考實例來操作時,Task、Command 的 this 願意共用相同的位址,而這個位址會是同時繼承了 Task、Command 的子類位址,也就是 Service 實例的位址。

