虚拟多重继承 - 最终覆盖

时间:2012-04-15 23:00:59

标签: c++ multiple-inheritance virtual-inheritance

在尝试更深入地分析C ++的继承机制时,我偶然发现了以下示例:

#include<iostream>

using namespace std;

class Base {
public:
    virtual void f(){
    cout << "Base.f" << endl; 
    }
};

class Left : public virtual Base {    
};

class Right : public virtual Base{
public:
    virtual void f(){
        cout << "Right.f" << endl; 
    }
};

class Bottom : public Left, public Right{

};

int main(int argc,char **argv)
{
    Bottom* b = new Bottom();
    b->f();
}

以上,不知何故,编译并调用Right :: f()。我看到编译器可能会发生什么,它知道有一个共享的Base对象,并且Right覆盖f(),但实际上,在我的理解中,应该有两个方法:Left::f()(继承来自Base::f())和Right::f(),其覆盖Base::f()。现在,我认为,基于Bottom继承了两个单独的方法,两个都有相同的签名,应该有冲突。

有没有人可以解释C ++的哪些规范细节处理这种情况以及它是如何从低级别的角度来处理的呢?

1 个答案:

答案 0 :(得分:4)

dreaded diamond 中有一个基础,两个中间对象从中派生出来,然后第四个类型用中间层中两种类型的多重继承关闭菱形。

您的问题似乎是在前一个示例中声明了多少f个函数?,答案是一个。

让我们从简单的基础和派生的线性层次结构的例子开始:

struct base {
   virtual void f() {}
};
struct derived : base {
   virtual void f() {}
};

在此示例中,声明了一个f,其中有两个覆盖base::fderived::f。在类型为derived的对象中,最终的覆盖为derived::f。值得注意的是,两个f函数都代表一个具有多个实现的函数。

现在,回到原始示例,在右侧的行上,Base::fRight::f与被覆盖的相同函数的方式相同。因此,对于Right类型的对象,最终的覆盖是Right::f。现在,对于类型为Left的最终对象,最终的覆盖为Base::f,因为Left不会覆盖该函数。

当菱形被关闭,并且因为继承是virtual时,只有一个Base对象,它声明了一个f函数。在第二级继承中,Right使用自己的实现覆盖该函数,这是最派生类型Bottom的最终覆盖。

您可能希望在标准之外查看此内容,并了解编译器实际如何实现它。编译器在创建Base对象时,会向虚拟表中添加隐藏指针vptr。虚拟表保存指向 thunks 的指针(为简单起见,假设该表保存了指向函数的最终重写符的指针,[1])。在这种情况下,Base对象将不包含任何成员数据,只包含指向包含指向函数Base::f的指针的表的指针。

Left扩展Base时,会为Left创建一个新的vtable,并且该vtable中的指针将设置为此级别的f的最终覆盖,顺便提一下Base::f所以两个vtables中的指针(忽略trampolin)跳转到相同的实际实现。当正在构造类型为Left的对象时,首先初始化Base子对象,然后在Left成员(如果有)Base::vptr的初始化之前进行初始化指针被更新以引用Left::vtable(即Base中存储的指针指的是为Left定义的表。)

在钻石的另一面,为Right创建的vtable包含一个 thunk ,最终调用Right::f。如果要创建类型为Right的对象,则会发生相同的初始化过程,Base::vptr将指向Derived::f

现在我们到达最终对象Bottom。同样,为类型Bottom生成vtable,并且vtable与所有其他类型的情况一样,包含表示f的单个条目。编译器分析继承的层次结构并确定Right::f覆盖Base::f,并且左分支上没有等效覆盖,因此在Bottom的vtable中,指针代表{{1} }指f。同样,在构建Right::f对象期间,Bottom会更新以引用Base::vptr的vtable。

如您所见,所有四个vtable都有一个Bottom条目,程序中有一个 f,即使每个vtable中存储的值都是不同(最终的改写不同)。

[1] thunk 是一小段代码,如果需要,它会调整f指针(多重继承通常意味着需要它),然后将调用转发给实际覆盖。在单继承的情况下,this指针不需要更新, thunk 消失,vtable中的条目直接指向实际函数。