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

virtual method table for multiple-inheritance

我正在阅读这篇文章“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(),如果我们想访问B1class中的成员怎么办D::f2()??我相信 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 相同的地址) 哪个会指向 b2 in B?????

我们不能将 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()

* 其他实现可能会很好地传递此信息;然而,文章中讨论的多重继承实现并没有这样做。

鉴于您的 class 层次结构,类型 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() 可以使用 B2 类型的指针或 D 类型的指针来调用。鉴于运行时必须能够正确使用 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变为0x1f50030D::f2()。这与 sizeof B1B2D 匹配。 D 对象的 B2 子对象的偏移量是 16 (0x10)B::test_f2()this 的值,一个 B*,在调用被调度到 D::f2().

之前被 0x10 更改

我猜想DB2的偏移值存储在B2的vtable中。否则,通用函数调度机制无法在将调用调度到正确的虚函数之前正确更改 this 的值。