【问题标题】:Virtual dispatch implementation details虚拟调度实现细节
【发布时间】:2011-04-27 17:13:23
【问题描述】:

首先,我想明确一点,我确实理解 C++ 标准中没有 vtables 和 vptrs 的概念。但是我认为几乎所有实现都以几乎相同的方式实现虚拟调度机制(如果我错了,请纠正我,但这不是主要问题)。另外,我相信我知道虚函数是如何工作的,也就是说,我总能知道哪个函数会被调用,我只需要实现细节。

假设有人问我以下问题:
“你有带有虚函数 v1、v2、v3 的基类 B 和派生类 D:B,它覆盖函数 v1 和 v3 并添加了一个虚函数 v4。解释虚拟调度的工作原理”。

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

&B::v1
&B::v2
&B::v3

D 的 vtable 将包含

&D::v1
&B::v2
&D::v3
&D::v4 

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

问题是:
1.我上面的描述有什么错误吗?
2. 编译器如何知道f在vtable中的位置(请详细说明)
3. 这是否意味着如果一个类有两个基,那么它就有两个 vptr?在这种情况下发生了什么? (尝试以与我类似的方式描述,尽可能详细)
4. A 在顶部 B,C 在中间,D 在底部的菱形层次结构中发生了什么? (A是B和C的虚基类)

提前致谢。

【问题讨论】:

    标签: c++ vtable vptr


    【解决方案1】:

    1.我上面的描述有什么错误吗?

    一切都好。 :-)

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

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

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

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

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

    4. A 在顶部 B,C 在中间,D 在底部的菱形层次结构中发生了什么? (A是B和C的虚基类)

    简短的回答?绝对的地狱。你实际上继承了这两个基地吗?只有其中之一?他们都不是?最终,使用了为该类编写 vtable 的相同技术,但是如何完成这一点的方式千差万别,因为 如何 它应该如何完成并不是一成不变的。解决菱形层次结构问题here 有一个不错的解释,但与大多数情况一样,它是特定于供应商的。

    【讨论】:

    • @Travis:感谢您的回答。 1.我不同意你的观点:析构函数没有进行虚拟调度(标准明确提到了这一点)。 2. 好的 3. 好极了。 4. 我特别提到了 B 和 C 实际上都是从 A 派生的。再次感谢您的回答,不胜感激。
    • +1 总体来说,但是第一点是错误的:vptr被析构函数修改了。当从最派生类型到基类的层次结构中的每个析构函数完成时,它会以与构造函数相反的方式更新 vptr。你的例子很好,但语言不会保护你,它是未定义的行为。许多编译器会引入一个pure virtual method thunk,它会打印出错误信息(pure virtual method called)。试试吧。决定更新指针的基本原理是方法的最终覆盖器不能是被破坏的对象。
    • @David:由于例子被删了,我记不太清了,但应该不是未定义的行为。正确的是,如果我错了,但行为已定义,它只是调用基类的 cleanup()。
    • @Armen, @Oli:我在codepad 写了一个小sn-p。这是我在那里的第一篇文章,所以如果它不起作用,请告诉我,我将在其他地方生成代码。
    • Base1 被称为“主要基地”
    【解决方案2】:
    1. 我觉得不错
    2. 特定于实现,但大多数只是按照源代码顺序——即它们在类中出现的顺序——从基类开始,然后从派生类中添加新的虚函数。只要编译器有一种确定的方式来做这件事,那么它想做的任何事情都可以。但是,在 Windows 上,要创建与 COM 兼容的 V-Table,它必须按源顺序进行

    3. (不确定)

    4. (猜测)菱形只是意味着您可以拥有基类 B 的两个副本。虚拟继承会将它们合并到一个实例中。所以如果你通过 D1 设置成员,你可以通过 D2 读取它。 (其中 C 源自 D1、D2,它们中的每一个都源自 B)。我相信在这两种情况下,vtables 都是相同的,因为函数指针是相同的——数据成员的内存是被合并的。

    【讨论】:

    • 抱歉,我对 3 和 4 没有明确的答案。我认为有一个步骤可以确保您拥有正确的 vtable,但我不知道细节。
    【解决方案3】:

    评论:

    • 我认为析构函数不会出现在其中!

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

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

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

    • Scott Meyers 的“Effective C++”书籍比我更好地解释了多重继承和菱形;我建议出于这个(以及许多其他)原因阅读它们。将它们视为必备读物!

    【讨论】:

    • @Oli:关于第一点 - 他们确实如此,否则你如何解释在 Base 的析构函数中调用虚函数 f 时,调用 Base::f 而不是 Derived::f?
    • 他是对的,析构函数不进行虚拟调用。为了在调用中执行此操作,它可能会替换他所描述的 vtable(需要注意的是,这都是特定于实现的)
    • @Armen:与来自Base 的任何其他成员函数内部的虚拟调用相同。
    • @Oli:恐怕你完全错了。如果您从任何其他基成员调用虚函数,则可能会根据对象的动态类型调用派生的覆盖
    • @Armen: On 可以根据对象的动态类型调用派生的覆盖 在层次结构中base derived rederived,在~rederived 完成后,使用rederived 类型的对象,对象的动态类型derived
    猜你喜欢
    • 2016-09-09
    • 2019-08-28
    • 2016-10-04
    • 1970-01-01
    • 1970-01-01
    • 2013-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多