C++ virtual table layout of MI(multiple inheritance)
看看下面的C++代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
virtual Base1 *clone() const;
protected:
float data_Base1;
};
class Base2 {
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base2 *clone() const;
protected:
float data_Base2;
};
class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived();
virtual Derived *clone() const;
protected:
float data_Derived;
}; |
"C++对象模型内部"4.2表示类Base1、Base2和派生类的虚拟表布局如下:。
氧化镁
我的问题是:
派生类的base1子对象的虚拟表包含Base2::mumble,为什么?我知道派生类与base1共享了这个虚拟表,所以我认为base2的函数不应该出现在这里。有人能告诉我为什么吗?谢谢。
- 在Base1的条目之后,向Derivedvtable添加其他条目不会造成伤害。这是为了提高效率。给定Derived*指针,通过Base1/Derivedvtable调用虚拟ffunctions比通过Base2vtable调用虚拟ffunctions便宜。
- 注:在Itanium abi中,_vptr成员实际上是第一个;同样,Base1是Derived的第一个成员。
- @马蒂厄姆。在我见过的所有编译器中,EDOCX1(伪)成员是类中的第一个成员,但标准显然允许它在任何地方使用。
- @Jameskanze:是的,这就是为什么我对我所说的ABI(也是我略知一二的唯一一个ABI)进行了估值;但在我看来,为了使优化更为相关,您希望尽可能容易地找到Base1的地址(因此,_vptr);理想情况下,不涉及任何算法。
- @马蒂厄姆。您希望尽可能容易地找到所有_vptr的地址。说真的,对于英特尔来说,有一个理由可以把_vptr放在第一个基类的末尾,并将指向它的指针作为指向对象的指针。(没有规定,或者至少过去没有规定,Derived*必须指向对象的第一个字节。)Intel有一种寻址模式,其中到基指针的偏移量可以是一个单字节,在-128...127的范围内。将指向对象的指针放在对象的中间意味着您可以更多地使用它。
- @ JamesKanze有这个老Metrowerks Codewarrior C/C++用于MaOS 7,最后VPTR。(我想是1996年那一轮。)
首先,我要提醒大家,实现多态性的解决方案的设计是一个超出标准的ABI决策。例如,MSVC和ItaniumABI(后面跟着GCC、Clang、ICC等)有不同的实现方法。
如果不这样做,我认为这是一个查找优化。
当您有一个Derived对象(或其后代之一)并查找mumble成员时,您不需要实际查找Base2子对象,而是可以直接从Base1子对象(其地址与Derived子对象一致,因此不涉及算术)。
- 从根本上讲,编译器在Base1的函数后面添加了Derived的函数,这样同一个vtable可以同时用于这两个函数。mumble也是Derived的功能(通过继承),而不仅仅是Base2的功能。这是一个"优化",但它是一个基于语言基础的优化。我无法想象有任何编译器没有这样做。
- 它真的是一个有用的优化吗?调用mumble时,仍然需要为其this指针找到Base2子对象,因此没有保存任何工作。唯一可能的改进是,如果您经常检查mumble的地址而不调用它,或者如果它提高了第一个子对象vtable上的缓存命中率。
- @无用:我必须承认,我不确定它是否有效;但是它确实消除了数据依赖性。现在可以并行计算mumble和Base2子对象的地址。因此,CPU可以计算mumble,开始从内存加载代码,并并行计算Base2子对象。
- @马修:有趣的一点!C++标准有什么说明吗?如果它像class Derived : public Base2, public Base1那样改变了顺序会发生什么?
- @纳亚纳达索里亚:不,正如我所说,标准只考虑效果,而不是手段。如果您将Base2和Base1互换,那么它们在内存布局中出现的顺序将改变,Base2的虚拟表将使用Base1方法进行扩展(最有可能)。
- 我想这只是一个打字错误。我猜派生的vtable实际上是由代表base1和base2 vtables的2个段构成的,所以要从派生的观点调用任何虚函数(如果我们知道对象确实是派生的),我们需要相同的指针运算(从整个派生vtable的开头偏移),我也需要查找另一个拼写错误:derived::~clone()而不是derived::clone()(位于被询问行之前)
- @用户396672:我不确定您是否可以有一个v-table,因为v-table以typeinfo条目开头(总是)。
- @Matthieu:这当然是一个编译器/ABI的决定,但从技术上讲,似乎没有什么能阻止在整个派生vtable中的任何地方在函数中放置任何数量的typeinfo(尽管我也不确定)。
- @Matthieu:…至少没有什么能阻止编译器将派生的/base1和派生的/base2表放在一个连续的空间中,因为编译器可以自由地将它们放在任何地方。顺便说一句,图片中还有另一个错误:显然,派生表不能与base1"共享"vtable,因为表中的内容不同(可能是"与派生表/base1共享"的意图)。老实说,我不能完全相信这样的消息来源…
- @用户396672:这不是一个错误。Derived和Base1有不同的v表,但是Base1中的v-ptr可以指向Derived表,因为Base1表是Derived表的前缀(Base1只使用它知道的前缀)。
- @Matthieu:我同意这可以被视为术语不一致(不是错误):第二个vtable被称为derived/base2,但第一个vtable只是base1,它是双重含义(base1原始vtable或derived/base1子对象的vtable)
- @我认为vtable会包含D::mumble,而不是Base2::mumble。无需指针调整。
在运行时,当您得到:
1 2 3
| Base2 b2;
Base1* b1_ptr = (Base1*)&b2;
b1_ptr->mumble(); // will call Base2::mumble(), this is the reason. |
然后需要调用base2::mumble()!注意,mumble()是层次结构中唯一被重写的虚拟方法。(甚至,您可能认为clone()也被重写,但它在类之间返回不同的类型,那么它是另一个签名)。
- 我很困惑,这难道不是一个编译器错误吗,因为base1没有名为mumble的函数?