虚拟继承:当只有一个基类具有“虚拟”关键字时,为什么它起作用?有没有更好的办法?

the*_*ker 2 c++ inheritance

我想在C ++中实现一个类层次结构:

  • 我需要接口,所以我可以提供多种实现。
  • 我需要所有类中的通用方法。但是我需要能够覆盖特定的方法。
  • 构造函数均采用至少一个参数。

简化我有此代码:

#include <iostream>

class IClass {
public:
    virtual int commonMethod() const = 0;
};

class Class : public virtual IClass {
protected:
    int commonValue;

public:
    Class(int commonValue) : commonValue(commonValue) {}

    virtual int commonMethod() const {
        return commonValue;
    }
};


class IClassDerived : public virtual IClass {
public:
    virtual void specialMethod() = 0;
};

class ClassDerived : public Class, public virtual IClassDerived {
public:
    ClassDerived(int commonValue) : Class(commonValue) {}

    virtual void specialMethod() {
        // do something
    }
};


class IClassDerived2 : public virtual IClassDerived {
public:
    virtual void specialMethod2() = 0;
};

class ClassDerived2 : public ClassDerived, public virtual IClassDerived2 {
public:
    ClassDerived2(int commonValue) : ClassDerived(commonValue) {}

    virtual void specialMethod2() {
        specialMethod();
    }
};


class IClassDerived3 : public virtual IClassDerived2 {
public:
    virtual int commonMethod() const override = 0;
};

class ClassDerived3 : public ClassDerived2, public virtual IClassDerived3 {
public:
    ClassDerived3(int commonValue) : ClassDerived2(commonValue) {}

    virtual int commonMethod() const override {
        return 4711;
    }
};


int main() {
    ClassDerived foo(1);
    ClassDerived2 foo2(2);
    ClassDerived3 foo3(3);

    std::cout << foo.commonMethod() << " " << foo2.commonMethod() << " " << foo3.commonMethod() << " " << std::endl;
    // 1 2 4711

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我现在有两个问题:

  • 为什么这完全起作用?
    • 如果我尝试不进行虚拟继承,则会出现错误“'specialMethod'是模棱两可的”和“ ...,因为以下虚拟函数在'ClassDerived'中是纯净的:virtual int IClass :: commonMethod()const”。由于存在两个基类,因此每个成员都存在两次,从而导致这些错误。好。
    • 如果我进行虚拟继承,并用“ public virtual”指定两个基类,则会得到“没有匹配的函数来调用'Class :: Class()'”。研究表明,在虚拟继承的情况下,我需要基类的默认构造函数。
    • 通过反复试验,我找到了上面的解决方案。但是我不明白为什么到目前为止它仍然有效。当只有一个基类是“虚拟的”而不是另一个基类时,会发生什么?
  • 有没有更好的办法?我设法得到了这个小例子,但实际上我的课程比较复杂,我担心这只能在这个小片段中起作用,而且我看不到将来可能会出现的问题...

dyp*_*dyp 5

我最近发现了一种不需要虚拟继承的解决方法(请参见下文)。

基本上,在这种情况下,这种语言需要使用虚拟继承直接解决问题,因为您从同一个类继承了多次。没有虚拟继承,您将得到以下结果:

接口0接口0接口0
    ^ ^ ^ ______
    | | \
接口1接口1 Impl0
    ^ ^ __________________ ^
    | \ |
接口2 Impl1
    ^ ______________________________ ^
                                   \ |
                                    Impl2

InterfaceX基类有多个独立的“实例” 。考虑Interface0路径中的实例Impl1 -> Interface1 -> Interface0。本Impl0类不继承实例Interface0,因此它并没有实现其虚函数。请注意,如果所有这些接口类都是有状态类(具有数据成员),而不是纯接口,则这很有用。

但是在这种特殊情况下,您仅从接口继承,理论上就不需要虚拟继承。我们想看下图:

接口0 _
    ^ | \
    | \
接口1 _ Impl0
    ^ | \ ^
    | \ |
接口2 _ Impl1
          | \ ^
            \ |
             Impl2

从理论上讲,Impl1可以定义与来自条目单个虚表Impl0从执行虚拟函数Interface0,并从功能上Impl1从执行虚拟功能Interface1。结果将是单个vtable,不需要进行偏移量计算(因此不需要虚拟继承)。

但是,可惜,这种语言没有以这种方式定义继承-它在抽象类和纯接口之间没有区别。如果您通过虚拟继承继承Impl0虚拟函数,Impl1 -> Interface1 -> Interface0则它仅允许您通过侧向继承来覆盖虚拟函数(覆盖的虚拟函数)。在几乎继承,你指定你确实只继承一次Interface0,所以从两个路径Impl1Interface0(直接继承和通过Impl0)产生相同的类。

虚拟继承有几个缺点,因为它必须允许仅在运行时才能确定基类(相对于子对象)的位置的情况。但是,有一种解决方法不需要虚拟继承。

首先编写一个自包含的类,然后将其改编成接口通常会更有用。该接口通常由周围的体系结构定义,因此不一定是该类的通用对象。通过将接口与实现分开,您可以使用其他接口重用实现。

如果我们将它们放在一起:我们不能使用多重继承来横向实现虚拟函数,而我们想要将接口与实现分开。我们最终得出:或者,要么不让我们的实现派生任何东西(这导致样板代码),要么我们线性地派生这是从顶部接口获得的唯一继承。

通过将实现类编写为类模板,我们可以进行线性派生并将顶部的派生接口传递给基本实现类:

struct Interface0 {
    virtual void fun0() = 0;
};

struct Interface1 : Interface1 {
    virtual void fun1() = 0;
};

struct Interface2 : Interface0 {
    virtual void fun2() = 0;
};


template<typename Interface = Interface0>
struct Impl0 : Interface {
    void fun0() {}
};

template<typename Interface = Interface1>
struct Impl1 : Impl0<Interface> {
    void fun1() {}
};

template<typename Interface = Interface2>
struct Impl2 : Impl1<Interface> {
    void fun2() {}
};


int main()
{
    auto x = Impl2<Interface2>(); // or simply: Impl2<>()
    Interface2* p = &x;
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们将继承用于以下两个方面:就实现Impl1而言Impl0,以及将扩展接口传递给我们的基类。

在该情况下mainImpl2<>

接口0
    ^
    |
接口1
    ^
    |
接口2 _
          | \
            \
             Impl0 <接口2>
               ^
               |
             Impl1 <接口2>
               ^
               |
             Impl2 <接口2>

另一种情况Impl1<>

接口0
    ^
    |
接口1 _
          | \
            \
             Impl0 <接口1>
               ^
               |
             Impl1 <接口1>