关于c ++:虚拟调度实现细节

Virtual dispatch implementation details

首先,我想清楚地表明,我确实理解在C++标准中没有VTABLE和VPTR的概念。然而,我认为实际上所有实现都以几乎相同的方式实现虚拟调度机制(如果我错了,请纠正我,但这不是主要问题)。另外,我相信我知道虚拟函数是如何工作的,也就是说,我总是可以知道将调用哪个函数,我只需要实现细节。

假设有人问我:"您有带有虚拟函数v1、v2、v3的基类B和派生类D:B,它重写函数v1和v3并添加虚拟函数v4。解释虚拟调度的工作原理"。

我想这样回答:对于每个具有虚拟函数的类(在本例中是b和d),我们有一个单独的指向称为vtable函数的指针数组。b的vtable将包含

1
2
3
&B::v1
&B::v2
&B::v3

d的vtable将包含

1
2
3
4
&D::v1
&B::v2
&D::v3
&D::v4

现在类B包含一个成员指针vptr。D自然地继承了它,因此也包含了它。在b的构造函数和析构函数中,将vptr设置为指向b的vtable。在d的构造函数和析构函数中,将其设置为指向d的vtable。对多态类X的对象X上的虚拟函数F的任何调用都被解释为对x.vptr[f在vtables中的位置]的调用。

问题是:1。上面的描述有错误吗?2。编译器如何知道f在vtable中的位置(请详细说明)三。这是否意味着如果一个类有两个基,那么它就有两个vptr?在这种情况下发生了什么?(尽量用与我相似的方式描述,尽可能详细)4。在钻石等级中,A在顶部B,C在中间,D在底部会发生什么?(A是B和C的虚拟基类)

事先谢谢。


1。上面的描述有错误吗?

一切都好。-)

2。编译器如何知道f在vtable中的位置?

每个供应商都有自己的方法来实现这一点,但我总是将vtable看作是成员函数签名到内存偏移量的映射。所以编译器只是维护这个列表。

三。这是否意味着如果一个类有两个基,那么它就有两个vptr?在这种情况下发生了什么?

通常,编译器组成一个新的vtable,它由所有虚拟基的vtable和虚拟基的vtable指针按指定的顺序附加在一起组成。然后是派生类的vtable函数。这是非常特定于供应商的,但是对于class D : B1, B2,您通常会看到D._vptr[0] == B1._vptr

multiple inheritance

该图像实际上是用于组成对象的成员字段,但vtables可以由编译器以完全相同的方式(据我所知)组成。

4。在钻石等级中,A在顶部B,C在中间,D在底部会发生什么?(A是B和C的虚拟基类)

简短的回答?绝对的地狱。你实际上继承了这两个基地吗?只有一个?他们两个都没有?最后,使用了为类编写vtable的相同技术,但是这种方法千差万别,因为应该如何做根本不是一成不变的。这里有一个很好的解释来解决钻石等级问题,但是,像大多数情况一样,它是供应商特有的。


  • 我觉得不错
  • 实现是特定的,但大多数都只是按照源代码顺序——也就是它们在类中出现的顺序——从基类开始,然后从派生类添加新的虚拟函数。只要编译器有一种确定的方式来完成这项工作,那么它想要做的任何事情都是好的。但是,在Windows上,要创建与COM兼容的V表,必须按源代码顺序

  • (不确定)

  • (猜猜)菱形只是意味着您可以拥有一个基类B的两个副本。虚拟继承将它们合并到一个实例中。因此,如果通过d1设置成员,则可以通过d2读取它。(C源于d1,d2,每个源于b)。我相信在这两种情况下,vtables是相同的,因为函数指针是相同的——数据成员的内存是合并的。

  • 评论:

    • 我不认为会有毁灭性的东西!

    • 诸如D d; d.v1();之类的调用可能不会通过vtable实现,因为编译器可以在编译/链接时解析函数地址。

    • 编译器知道f的位置,因为它放在那里!

    • 是的,具有多个基类的类通常具有多个vptr(假设每个基类中都有虚拟函数)。

    • Scott Meyers的"有效C++"书解释了多重继承和钻石比我更好;我建议阅读他们的(和许多其他)原因。考虑他们的基本阅读!