用于多重继承的虚拟方法表

virtual method table for multiple-inheritance

本文关键字:方法 虚拟 多重继承 用于      更新时间:2023-10-16

我正在阅读这篇文章"Virtual method table"

以上文章中的例子:

class B1 {
public:
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};
class B2 {
public:
  virtual void f2() {}
  int int_in_b2;
};
class D : public B1, public B2 {
public:
  void d() {}
  void f2() {}  // override B2::f2()
  int int_in_d;
};
B2 *b2 = new B2();
D  *d  = new D();

在文章中,作者介绍了对象d的内存布局:

          d:
D* d-->      +0: pointer to virtual method table of D (for B1)
             +4: value of int_in_b1
B2* b2-->    +8: pointer to virtual method table of D (for B2)
             +12: value of int_in_b2
             +16: value of int_in_d
Total size: 20 Bytes.
virtual method table of D (for B1):
  +0: B1::f1()  // B1::f1() is not overridden
virtual method table of D (for B2):
  +0: D::f2()   // B2::f2() is overridden by D::f2()

问题是关于d->f2()的。对d->f2()的调用传递了一个B2指针作为this指针,所以我们必须这样做:

(*(*(d[+8]/*pointer to virtual method table of D (for B2)*/)[0]))(d+8) /* Call d->f2() */

为什么我们应该传递一个B2指针作为this指针而不是原来的D指针?我们实际上调用了D::f2()。根据我的理解,我们应该将D指针作为this传递给D::f2()函数。

___update____

如果将B2指针作为this传递给D::f2(),如果我们想在D::f2()中访问B1类的成员该怎么办?我相信B2指针(this)是这样显示的:

          d:
D* d-->      +0: pointer to virtual method table of D (for B1)
             +4: value of int_in_b1
B2* b2-->    +8: pointer to virtual method table of D (for B2)
             +12: value of int_in_b2
             +16: value of int_in_d

它已经有这个连续内存布局的起始地址的一定偏移量。例如,我们想在D::f2()中访问b1,我猜在运行时,它会做这样的事情:*(this+4) (this指向与b2相同的地址),这将指向B ????中的b2

不能将D指针传递给覆盖B2::f2()的虚函数,因为对同一个虚函数的所有覆盖必须接受相同的内存布局。

由于B2::f2()函数期望传递给它的对象的B2的内存布局作为其this指针,即

b2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

重写函数D::f2()也必须期望相同的布局。否则,函数将不再是可互换的。

要了解为什么互换性很重要,请考虑以下场景:

class B2 {
public:
  void test() { f2(); }
  virtual void f2() {}
  int int_in_b2;
};
...
B2 b2;
b2.test(); // Scenario 1
D d;
d.test(); // Scenario 2

B2::test()在两种场景下都需要呼叫f2()。它没有额外的信息来告诉它在进行这些调用*时如何调整this指针。这就是编译器传递修复指针的原因,因此test()f2的调用将同时用于D::f2()B2::f2()

*其他实现可以很好地传递此信息;然而,本文中讨论的多重继承实现并没有做到这一点。

给定您的类层次结构,类型为B2的对象将具有以下内存占用:

+------------------------+
| pointer for B2 vtable  |
+------------------------+
| int_in_b2              |
+------------------------+

类型为D的对象的内存占用如下:

+------------------------+
| pointer for B1 vtable  |
+------------------------+
| int_in_b1              |
+------------------------+
| pointer for B2 vtable  |
+------------------------+
| int_in_b2              |
+------------------------+
| int_in_d               |
+------------------------+

使用

D* d  = new D();
d->f2();

该调用与

相同
B2* b  = new D();
b->f2();

f2()可以使用类型为B2D的指针调用。考虑到运行时必须能够正确地使用类型为B2的指针,它必须能够使用B2的虚函数表中适当的函数指针正确地调度对D::f2()的调用。然而,当调用被分派到D:f2()时,B2类型的原始指针必须以某种方式正确偏移,以便在D::f2()中,this指向D,而不是B2

下面是您的示例代码,稍微修改了一下以打印有用的指针值和成员数据,以帮助理解各种函数中this值的变化。

#include <iostream>
struct B1 
{
   void f0() {}
   virtual void f1() {}
   int int_in_b1;
};
struct B2 
{
   B2() : int_in_b2(20) {}
   void test_f2()
   {
      std::cout << "In B::test_f2(), B*: " << (void*)this << std::endl;
      this->f2();
   }
   virtual void f2()
   {
      std::cout
         << "In B::f2(), B*: " << (void*)this
         << ", int_in_b2: " << int_in_b2 << std::endl;
   }
   int int_in_b2;
};
struct D : B1, B2 
{
   D() : int_in_d(30) {}
   void d() {}
   void f2()
   {
      // ======================================================
      // If "this" is not adjusted properly to point to the D
      // object, accessing int_in_d will lead to undefined 
      // behavior.
      // ======================================================
      std::cout
         << "In D::f2(), D*: " << (void*)this
         << ", int_in_d: " << int_in_d << std::endl;
   }
   int int_in_d;
};
int main()
{
   std::cout << "sizeof(void*) : " << sizeof(void*) << std::endl;
   std::cout << "sizeof(int)   : " << sizeof(int) << std::endl;
   std::cout << "sizeof(B1)    : " << sizeof(B1) << std::endl;
   std::cout << "sizeof(B2)    : " << sizeof(B2) << std::endl;
   std::cout << "sizeof(D)     : " << sizeof(D) << std::endl << std::endl;
   B2 *b2 = new B2();
   D  *d  = new D();
   b2->test_f2();
   d->test_f2();
   return 0;
}

程序输出:

sizeof(void*) : 8
sizeof(int)   : 4
sizeof(B1)    : 16
sizeof(B2)    : 16
sizeof(D)     : 32
In B::test_f2(), B*: 0x1f50010
In B::f2(), B*: 0x1f50010, int_in_b2: 20
In B::test_f2(), B*: 0x1f50040
In D::f2(), D*: 0x1f50030, int_in_d: 30

当实际调用test_f2()的对象为D时,this的值由test_f2()中的0x1f50040变为D::f2()中的0x1f50030。匹配sizeof B1, B2DD对象的B2子对象的偏移量为16 (0x10)B::test_f2() (B*)中的this的值被0x10修改后才被调度到D::f2()

我猜从DB2的偏移量的值存储在B2的虚表中。否则,在将调用调度到正确的虚函数之前,泛型函数调度机制无法正确地更改this的值。