虚拟继承


类若继承两个以上的抽象类,而两个抽象类都定义了相同方法,那么子类会怎样吗?程序面上来说,并不会有错误,照样通过编译:

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实例的地址。


展开阅读全文