了解多重继承中的虚拟表

Understanding virtual table in multiple inheritance

本文关键字:虚拟 多重继承 了解      更新时间:2023-10-16

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

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?我想它在某种程度上需要使用IFace2vptr,但不清楚具体如何使用。

警告:下面的大部分内容当然是实现的,依赖于平台并简化了。我将按照我在您的示例中看到的方式来实现它——可能是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+0bj
  • 如果他们想访问成员字段(自己访问,例如,如果该字段具有公共访问权限,或者存在内联访问器),他们只需在其地址obj+xxx读取/写入成员即可
  • 如果他们想知道自己真正的类型,他们会从对象的地址中减去vtbl-16处的值,然后查看基础对象引用的vtable的typeinfo指针

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

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

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

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

3)最后,如何处理该对象,以便其他人能够将其用作IFace2?已经创建的vtable不能再使用,因为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指针上调用RealClass中实现的这个方法,则this指向RealClass的基。与CCD_ 38相同。但是,如果它是由具有指向IFace2接口的指针的人调用的,那么this会将8个(或多个)字节指向对象!

因此,编译器需要多次生成函数来适应这种情况,否则它无法正确访问成员字段和其他方法,因为它会因调用该方法的人而异。

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

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