用于多重继承的虚拟方法 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(),如果我们想访问B1
class中的成员怎么办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
变为0x1f50030
在 D::f2()
。这与 sizeof B1
、B2
和 D
匹配。 D
对象的 B2
子对象的偏移量是 16 (0x10)
。 B::test_f2()
中 this
的值,一个 B*
,在调用被调度到 D::f2()
.
之前被 0x10
更改
我猜想D
到B2
的偏移值存储在B2
的vtable中。否则,通用函数调度机制无法在将调用调度到正确的虚函数之前正确更改 this
的值。
我正在阅读这篇文章“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(),如果我们想访问B1
class中的成员怎么办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
变为0x1f50030
在 D::f2()
。这与 sizeof B1
、B2
和 D
匹配。 D
对象的 B2
子对象的偏移量是 16 (0x10)
。 B::test_f2()
中 this
的值,一个 B*
,在调用被调度到 D::f2()
.
0x10
更改
我猜想D
到B2
的偏移值存储在B2
的vtable中。否则,通用函数调度机制无法在将调用调度到正确的虚函数之前正确更改 this
的值。