【问题标题】:Understanding virtual table in multiple inheritance了解多重继承中的虚拟表
【发布时间】:2014-06-23 15:11:06
【问题描述】:

我有一个实现两个抽象类的类,如下所示。没有虚拟继承。没有数据成员。

class IFace1 {
public:
    virtual void fcn(int abc) = 0;
};

class IFace2 {
public:
    virtual void fcn1(int abc) = 0;
};

class RealClass: public IFace1, public IFace2 {
public:
    void fcn(int a) {
    }

    void fcn1(int a) {
   }
};

我发现 RealClass 的 vtable 和对象内存布局如下所示。

Vtable for RealClass
RealClass::_ZTV9RealClass: 7u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI9RealClass)
16    (int (*)(...))RealClass::fcn
24    (int (*)(...))RealClass::fcn1
32    (int (*)(...))-8
40    (int (*)(...))(& _ZTI9RealClass)
48    (int (*)(...))RealClass::_ZThn8_N9RealClass4fcn1Ei

Class RealClass
    size=16 align=8
    base size=16 base align=8
RealClass (0x2af836d010e0) 0
    vptr=((& RealClass::_ZTV9RealClass) + 16u)
    IFace1 (0x2af836cfa5a0) 0 nearly-empty
        primary-for RealClass (0x2af836d010e0)
    IFace2 (0x2af836cfa600) 8 nearly-empty
        vptr=((& RealClass::_ZTV9RealClass) + 48u)

我对此感到困惑。什么是 RealClass::_ZThn8_N9RealClass4fcn1Ei?为什么 IFace2 的 vptr 指向那个?当我从 IFace2 * 调用 fcn1 时会发生什么?程序如何在RealClass的Vtable中找到RealClass::fcn1?我猜它不知何故需要使用 IFace2 vptr,但不清楚具体如何。

【问题讨论】:

  • 有很多方法可以实现这一点。 refspecs.linuxbase.org/cxxabi-1.83.html 是一个好的、有据可查的文件(但它可能与您使用的不匹配)。
  • 尝试查看为此类调用生成的代码?
  • RealClass::_ZThn8_N9RealClass4fcn1Ei 是一个RealClass::non-virtual thunk to RealClass::fcn1(int),它可能用于内联和其他调用优化。提示:c++filt 程序是你的朋友。
  • 我认为 RealClass::_ZThn8_N9RealClass4fcn1Ei 是 gcc 用于直接调用 RealClass::fcn1 的魔法函数。当我们使用 IFace2* 调用 fcn1 时,它只是调用其 vptr 所指向的函数

标签: c++


【解决方案1】:

警告:下面的大部分内容当然是实现和平台相关和简化的。我将按照我在您的示例中看到的实现方式进行操作——可能是 GCC,64 位。


首先,虚拟类实例的契约是什么?例如。如果你有一个变量IFace1* obj:

  • obj+0 处有一个指向虚拟表的指针。
  • 任何成员数据字段都将在 obj+8 (sizeof(void*)) 处继续。
  • 虚拟表包含一条记录,该记录在 vtbl+0 处指向 void fcn(int)
  • 在表中,还有一个指向 vtbl-8 类的typeinfo 的指针(由dynamic_cast 等使用)和“偏移到基数”在 vtbl-16

任何看到IFace1* 类型变量的函数都可以认为这是真的。 IFace2* 也是如此。

  • 如果要调用虚函数void fcn(int),查看obj+0获取vtable,然后在vtbl+0处调用找到的地址那里。 this 设置为 obj
  • 如果他们想要访问成员字段(通过他们自己,例如如果该字段具有公共访问权限,或者如果有内联访问器),他们只需在其地址 obj+xxx强>。
  • 如果他们想看看他们真正有什么类型,他们从他们对象的地址中减去 vtbl-16 处的值,然后查看被引用的 vtable 的 typeinfo 指针基础对象。

现在,对于具有多重继承的类,编译器如何满足这些要求?

1) 首先它需要为自己生成结构。虚拟表指针必须在obj+0,所以它就在那里。桌子会是什么样子?嗯,base 的偏移量 是 0,很明显,typeinfo 数据和指向它的指针很容易生成,然后是第一个虚函数和第二个虚函数,没什么特别的。任何知道RealClass 定义的人都可以做同样的计算,所以他们知道在哪里可以找到vtable 等中的函数。

2) 然后它可以让RealClass 作为IFace1 传递。所以它需要在对象的某处有一个指向IFace1格式的虚拟表的指针,然后虚拟表必须有void fcn(int)的那条记录。

编译器很聪明,它认为它可以重用它生成的第一个虚拟表,因为它符合这些要求。如果有任何成员字段,它们将存储在指向虚拟表的第一个指针之后,因此即使是它们也可以简单地访问,就好像派生类是基类一样。到目前为止一切顺利。

3) 最后,如何处理该对象以便其他人能够将其用作IFace2?已经创建的一个 vtable 不能再使用了,因为 IFace2 需要它的 void fcn1(int) 位于 vtbl+0

因此创建了另一个虚拟表,您在转储中看到的紧跟在第一个虚拟表之后,指向它的指针存储在下一个可用位置的RealClass 中。第二个表需要将 offset to base 设置为 -8,因为真实对象从偏移量 -8 开始。它只包含指向IFace2 虚函数void fcn1(int) 的指针。

对象中的虚拟指针(偏移量obj+8)之后会跟IFace2 的任何成员数据字段,因此任何继承的或内联的函数在给出指向此接口的指针。


好的,现在有人怎么能从IFace2 呼叫fcn1()?那是什么non-virtual thunk to RealClass::fcn1(int)

如果你将RealClass* 指针传递给一个接受IFace2* 的陌生函数,编译器将发出代码以将你的指针增加8(或者sizeof(void*) + sizeof(IFace1) 的大小),以便函数获取指针从IFace2 的虚拟表指针开始,然后是它的成员字段——正如我之前概述的合同中所约定的那样。

当该函数想要调用void IFace2::fcn1(int) 时,它会查看虚拟表,转到该特定函数的记录(第一个也是唯一一个)并调用它,并将this 设置为传递的地址指向IFace2的指针。

这里出现了一个问题:如果有人在RealClass 指针上调用在RealClass 中实现的这个方法,this 指向RealClass 的基数。与IFace1 相同。但是,如果它是由具有指向 IFace2 接口的指针的人调用的,则 this 会将 8 个(或许多)字节指向对象!

所以编译器需要多次生成函数来适应这一点,否则它无法正确访问成员字段和其他方法,因为它因调用方法而异。

编译器并没有真正让代码重复两次,而是通过创建隐藏的隐式小 thunk 函数来优化这一点,这只是

  1. 适当减少this指针,
  2. 调用真正的方法,现在无论是谁调用它都可以正常工作。

【讨论】:

    猜你喜欢
    • 2017-03-31
    • 2015-11-21
    • 1970-01-01
    • 1970-01-01
    • 2014-10-12
    • 2013-11-19
    • 1970-01-01
    • 2011-10-15
    • 1970-01-01
    相关资源
    最近更新 更多