虚拟决赛覆盖器

Virtual final overrider

本文关键字:覆盖 虚拟      更新时间:2023-10-16

在试图更深入地分析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++的哪个规范细节处理这种情况,以及它是如何从低级角度处理的?

可怕的钻石中,有一个基底,两个中间对象从中派生出来,然后第四种类型通过中间层中两种类型的多重继承来关闭钻石。

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

让我们从更简单的线性层次结构示例开始,该线性层次结构仅包含 base 和 派生:

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到虚拟表。虚拟表包含指向 thunk 的指针(为简单起见,假设该表包含指向函数最终覆盖器的指针 [1])。在这种情况下,Base对象将不包含任何成员数据,而只包含一个指向表的指针,该表包含指向函数Base::f的指针。

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

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

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

如您所见,所有四个 vtable 都有一个 f 条目,程序中只有一个f,即使存储在每个 vtable 中的值不同(最终覆盖器不同)。

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