Xin*_*nus 17 c++ multiple-inheritance virtual-inheritance vtable vptr
我试图理解书中有效的c ++语句.以下是多继承的继承图.


现在这本书说vptr需要每个类中的单独内存.它也做了以下声明
上图中的一个奇怪之处在于,即使涉及四个类,也只有三个vptrs.如果愿意,实现可以自由地生成四个vpt,但是三个就足够了(事实证明B和D可以共享一个vptr),并且大多数实现利用这个机会来减少编译器生成的开销.
我看不出有什么理由为什么每个类都要求为vptr提供单独的内存.我理解vptr是从基类继承的,可能是继承类型.如果我们假设它显示了带有继承的vptr的结果内存结构,那么它们如何才能生成该语句
B和D可以共享vptr
有人可以在多重继承中澄清一下vptr吗?
Mat*_* M. 23
你的问题很有意思,但是我担心你的目标太大而不是第一个问题,所以如果你不介意的话,我会分几步回答:)
免责声明:我不是编译器作者,虽然我确实研究了这个主题,但我的话应该谨慎对待.我会有不准确之处.而且我并不精通RTTI.此外,由于这不是标准,我所描述的是可能性.
1.如何实现继承?
注意:我将省略对齐问题,它们只是意味着可以在块之间包含一些填充
现在让我们把它留给虚拟方法,并专注于如何实现继承,如下所示.
事实是,继承和构成分享了很多:
struct B { int t; int u; };
struct C { B b; int v; int w; };
struct D: B { int v; int w; };
Run Code Online (Sandbox Code Playgroud)
看起来像是这样的:
B:
+-----+-----+
| t | u |
+-----+-----+
C:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
D:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
Run Code Online (Sandbox Code Playgroud)
令人震惊的不是它:)?
但是,这意味着要比多重继承更容易理解:
struct A { int r; int s; };
struct M: A, B { int v; int w; };
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
Run Code Online (Sandbox Code Playgroud)
使用这些图表,让我们看看在将派生指针转换为基指针时会发生什么:
M* pm = new M();
A* pa = pm; // points to the A subpart of M
B* pb = pm; // points to the B subpart of M
Run Code Online (Sandbox Code Playgroud)
使用我们之前的图表:
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
^ ^
pm pb
pa
Run Code Online (Sandbox Code Playgroud)
地址与pb稍有不同的事实pm由编译器自动通过指针算法处理.
2.如何实现虚拟继承?
虚拟继承很棘手:您需要确保V所有其他子对象共享一个(用于虚拟)对象.让我们定义一个简单的钻石继承.
struct V { int t; };
struct B: virtual V { int u; };
struct C: virtual V { int v; };
struct D: B, C { int w; };
Run Code Online (Sandbox Code Playgroud)
我将省略表示,并专注于确保在D对象中,子组件B和C子组件共享相同的子对象.怎么做到呢 ?
已发现该溶液是简单因此:B与C只保留空间的指针V,和:
B,构造函数将V在堆上分配一个,它将自动处理B为a的一部分D,B子部分将期望D构造函数将指针传递给的位置V而对于同上C,效果显着.
在D,优化允许构造预留空间V权的对象,因为D没有实际上无论从继承B或者C,给你证明(虽然我们还没有的虚方法)的图.
B: (and C is similar)
+-----+-----+
| V* | u |
+-----+-----+
D:
+-----+-----+-----+-----+-----+-----+
| B | C | w | A |
+-----+-----+-----+-----+-----+-----+
Run Code Online (Sandbox Code Playgroud)
备注现在从铸造B到A比简单的指针运算稍微棘手:您需要按照指针B,而不是简单的指针运算.
然而,更糟糕的情况是,提升.如果我给你一个指针,A你怎么知道怎么回去B?
在这种情况下,魔术是由执行dynamic_cast,但这需要一些存储在某处的支持(即信息).这就是所谓的RTTI(运行时类型信息).dynamic_cast将首先确定A是一部分D通过一些魔法,然后查询D的运行信息,知道内D的B子对象存储.
如果我们遇到没有B子对象的情况,它将返回0(指针形式)或抛出bad_cast异常(引用形式).
3.如何实现虚拟方法?
通常,虚拟方法是通过每个类的v表(即,指向函数的指针表)实现的,并且每个对象都是v-ptr到该表.这不是唯一可能的实现,并且已经证明其他可能更快,但它既简单又具有可预测的开销(在内存和调度速度方面).
如果我们使用虚方法获取一个简单的基类对象:
struct B { virtual foo(); };
Run Code Online (Sandbox Code Playgroud)
对于计算机,没有成员方法这样的东西,所以实际上你有:
struct B { VTable* vptr; };
void Bfoo(B* b);
struct BVTable { RTTI* rtti; void (*foo)(B*); };
Run Code Online (Sandbox Code Playgroud)
当你从以下来源B:
struct D: B { virtual foo(); virtual bar(); };
Run Code Online (Sandbox Code Playgroud)
您现在有两个虚拟方法,一个覆盖B::foo,另一个是全新的.计算机表示类似于:
struct D { VTable* vptr; }; // single table, even for two methods
void Dfoo(D* d); void Dbar(D* d);
struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };
Run Code Online (Sandbox Code Playgroud)
注意如何BVTable和DVTable如此相似(因为我们foo之前提出bar)?这一点很重要!
D* d = /**/;
B* b = d; // noop, no needfor arithmetic
b->foo();
Run Code Online (Sandbox Code Playgroud)
让我们将调用转换foo为机器语言(有点):
// 1. get the vptr
void* vptr = b; // noop, it's stored at the first byte of B
// 2. get the pointer to foo function
void (*foo)(B*) = vptr[1]; // 0 is for RTTI
// 3. apply foo
(*foo)(b);
Run Code Online (Sandbox Code Playgroud)
那些vptrs由对象的构造函数初始化,在执行构造函数时D,这里发生了什么:
D::D()电话B::B()首先,要initiliaze其子部分B::B()初始化vptr以指向其vtable,然后返回D::D()初始化vptr指向其vtable,覆盖B的因此,vptr这里指的是D's vtable,因此foo应用的是D's.因为B它完全透明.
这里B和D共享相同的vptr!
4.多继承中的虚拟表
不幸的是,这种共享并非总是可行.
首先,正如我们所看到的,在虚拟继承的情况下,"共享"项在最终完整对象中奇怪地定位.因此它有自己的vptr.那是1.
其次,在多继承的情况下,第一个基础与完整对象对齐,但第二个基础不能(它们都需要空间用于它们的数据),因此它不能共享其vptr.那是2.
三,第一基础是与完整的对象对齐,从而为我们提供相同的布局,在简单的继承(相同的优化机会)的情况下.那是3.
很简单,不是吗?
如果一个类有虚拟成员,则需要找到他们的地址。它们被收集在一个常量表(vtbl)中,其地址存储在每个对象(vptr)的隐藏字段中。对虚拟成员的调用本质上是:
obj->_vptr[member_idx](obj, params...);
Run Code Online (Sandbox Code Playgroud)
将虚拟成员添加到基类的派生类也需要一个位置。因此为他们提供了一个新的 vtbl 和一个新的 vptr。对继承的虚拟成员的调用仍然是
obj->_vptr[member_idx](obj, params...);
Run Code Online (Sandbox Code Playgroud)
对新虚拟成员的调用是:
obj->_vptr2[member_idx](obj, params...);
Run Code Online (Sandbox Code Playgroud)
如果基址不是虚拟的,则可以将第二个 vtbl 紧接在第一个 vtbl 之后放置,从而有效地增加 vtbl 的大小。并且不再需要 _vptr2。对新虚拟成员的调用如下:
obj->_vptr[member_idx+num_inherited_members](obj, params...);
Run Code Online (Sandbox Code Playgroud)
在(非虚拟)多重继承的情况下,一个继承两个vtbl和两个vptr。它们不能合并,调用时必须注意给对象添加偏移量(以便在正确的位置找到继承的数据成员)。对第一个基类成员的调用将是
obj->_vptr_base1[member_idx](obj, params...);
Run Code Online (Sandbox Code Playgroud)
对于第二个
obj->_vptr_base2[member_idx](obj+offset, params...);
Run Code Online (Sandbox Code Playgroud)
新的虚拟成员可以再次放入新的 vtbl 中,或附加到第一个基础的 vtbl 中(以便在将来的调用中不会添加偏移量)。
如果基础是虚拟的,则无法将新的 vtbl 附加到继承的 vtbl 上,因为这可能会导致冲突(在您给出的示例中,如果 B 和 C 都附加其虚拟函数,那么 D 如何能够构建其版本?) 。
因此,A需要一个vtbl。B 和 C 需要一个 vtbl,并且它不能附加到 A 的 vtbl 上,因为 A 是两者的虚拟基。D 需要一个 vtbl,但它可以附加到 B 一个,因为 B 不是 D 的虚拟基类。