多重继承中的虚拟表解析

3
我有一个类实现了两个抽象类,如下所示。没有虚继承。没有数据成员。
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 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中找到fcn1?我猜它需要使用IFace2的vptr,但不清楚具体是如何实现的。


1
有很多实现这个的方法。一个好的、有良好文档的方法是http://refspecs.linuxbase.org/cxxabi-1.83.html(但它可能与您使用的不匹配)。 - Alan Stokes
1
尝试查看为此调用生成的代码? - Alan Stokes
RealClass::_ZThn8_N9RealClass4fcn1Ei 是一个 RealClass::non-virtual thunk to RealClass::fcn1(int),可能旨在进行内联和其他调用优化。提示:c++filt 程序是您的好朋友。 - user3159253
我认为RealClass::_ZThn8_N9RealClass4fcn1Ei是gcc用来指导函数调用到RealClass::fcn1的魔法函数。当我们使用IFace2*调用fcn1时,它只是调用由其vptr指向的函数。 - szli
1个回答

4

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


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

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

任何看到IFace1*类型变量的函数都可以依赖于这一点。对于IFace2*同样适用。

  • 如果他们想调用虚拟函数void fcn(int),则查看obj+0以获取虚拟表,然后查看vtbl+0并调用找到的地址。将this设置为obj
  • 如果他们想访问成员字段(自己访问,例如,如果字段具有公共访问权限,或者如果存在内联访问器),则只需读取/写入其地址obj+xxx处的成员即可。
  • 如果他们想知道实际类型是什么,则从对象地址减去vtbl-16处的值,然后查看基础对象引用的虚拟表的typeinfo指针。

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

1) 首先,它需要为自己生成结构。虚拟表指针必须位于obj + 0处,因此在那里。表将是什么样子呢?嗯,偏移量为0,显然,typeinfo数据和指针很容易生成,然后是第一个虚拟函数和第二个虚拟函数,没什么特别的。任何知道RealClass定义的人都可以进行相同的计算,因此他们知道在vtable中找到函数的位置等。

2) 然后它使得RealClass能够以IFace1的形式传递。因此,在对象中需要有一个指向虚表的指针,格式为IFace1,然后虚表必须具有void fcn(int)的一条记录。

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

3) 最后,如何让其他人能够将该对象用作IFace2?不能再使用已经创建的虚表,因为IFace2需要其void fcn1(int)位于vtbl+0处。

因此,另一个虚表被创建,您在转储中立即看到它,指向它的指针存储在RealClass中的下一个可用位置。这个第二个虚表需要将基础偏移量设置为-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 的指针上调用此方法,this 指向 RealClass 的基类。对于 IFace1 也是一样的。但如果由具有指向 IFace2 接口的指针的人调用它,则 this 将指向对象内部的 8(或多个)字节!
因此,编译器需要多次生成该函数以适应这种情况,否则无法正确访问成员字段和其他方法,因为这取决于谁调用该方法。
为了避免代码实际上重复,编译器通过创建隐藏的隐式小thunk函数来优化此过程,该函数只需进行以下操作:
  1. 按正确的数量减少 this 指针,
  2. 调用真正的方法,现在可以正常工作,而不管是谁调用的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接