菱形继承来自于多重继承,是为了解决具有歧义的组合而产生的一个结果。通过引入虚继承,使得多个由虚基类直接或间接派生的类拥有一个共同的基类实例。实现上相当于派生类绕过了父类直接继承了祖父类(虚基类)。
下面我们通过 MSC 14 编译器(VS2015)来看一下菱形继承的实现细节。

类继承结构如下所示:

class A0
{
public:
    virtual      ~A0()  { }
    virtual void f()    = 0;
    virtual void f2()   = 0;
    int ma0;
};

class A1
{
public:
    virtual      ~A1() { printf("%d", ma1); }
    virtual void g()   = 0;
    virtual void g2()  = 0;
    int ma1;
};

class B : virtual public A0, virtual public A1
{
public:
    virtual void f() { printf("B::f"); }
    virtual void b() { printf("B::b"); }
    int mb;
};

class C : virtual public A0, virtual public A1
{
public:
    virtual void f() { printf("C::f"); }
    virtual void c() { printf("C::c"); }
    int mc;
};

class D : public B, public C
{
public:
    virtual void f()  { printf("D::f");  }
    virtual void f2() { printf("D::f2"); }
    virtual void g()  { printf("D::g");  }
    virtual void g2() { printf("D::g2"); }
    virtual void b()  { printf("D::b");  }
    virtual void c()  { printf("D::c");  }
    virtual void d()  { printf("D::d");  }
    int md;
};

首先使用 -d1reportAllClassLayout 编译选项查看一下所有类的内存布局:

class A0 size(8):				class A1 size(8):
	+---							+---
 0	| {vfptr}					 0	| {vfptr}		// A0、A1 的虚表指针
 4	| ma0						 4	| ma1			// A0、A1 的成员
	+---							+---

A0::$vftable@:					A1::$vftable@:
 0	| &A0::{dtor}				 0	| &A1::{dtor}	// 各自的虚表以及函数的偏移
 1	| &A0::f					 1	| &A1::g
 2	| &A0::f2					 2	| &A1::g2
 

class B size(28):				B::$vftable@B@:		// B 自身的虚表
	+---						 0	| &B::b
 0	| {vfptr}
 4	| {vbptr}					B::$vbtable@:		// B 的虚基类表,由偏移 4 处
 8	| mb											// 的 vbptr 指向
	+---						 0	| -4			// 第一项为自身(B) 相对
	+--- (virtual base A0)							// 于 vbptr 的偏移,即为 0
12	| {vfptr}					 1	| 8 (Bd(B+4)A0)	// 第一个虚基类 A0 的偏移,12
16	| ma0						 2	| 16 (Bd(B+4)A1)// A1 的偏移,20
	+---						
	+--- (virtual base A1)		B::$vftable@A0@:	// B 中来自于 A0 的虚函数
20	| {vfptr}					 0	| &B::{dtor}
24	| ma1						 1	| &B::f
	+---						 2	| &A0::f2
								
								B::$vftable@A1@:	// B 中来自于 A1 的虚函数
								 0	| &thunk: this-=8; goto B::{dtor}
								 1	| &A1::g
								 2	| &A1::g2


// C 同 B
class C size(28):				C::$vftable@C@:
	+---						 0	| &C::c
 0	| {vfptr}
 4	| {vbptr}					C::$vbtable@:
 8	| mc						 0	| -4
	+---						 1	| 8 (Cd(C+4)A0)
	+--- (virtual base A0)		 2	| 16 (Cd(C+4)A1)
12	| {vfptr}
16	| ma0						C::$vftable@A0@:
	+---						 0	| &C::{dtor}
	+--- (virtual base A1)		 1	| &C::f
20	| {vfptr}					 2	| &A0::f2
24	| ma1
	+---						C::$vftable@A1@:
								 0	| &thunk: this-=8; goto C::{dtor}
								 1	| &A1::g
								 2	| &A1::g2



class D size(44):				D::$vftable@B@:		// 来自 B 的虚函数
	+---						 0	| &D::b
 0	| +--- (base class B)
 0	| | {vfptr}					D::$vftable@C@:		// 来自 C 的虚函数
 4	| | {vbptr}					 0	| &D::c
 8	| | mb
	| +---						D::$vbtable@B@:		// B 的虚基类表
12	| +--- (base class C)		 0	| -4
12	| | {vfptr}					 1	| 24 (Dd(B+4)A0)
16	| | {vbptr}					 2	| 32 (Dd(B+4)A1)
20	| | mc
	| +---						D::$vbtable@C@:		// C 的虚基类表
24	| md						 0	| -4
	+---						 1	| 12 (Dd(C+4)A0)// 计算得到的 A0、A1
	+--- (virtual base A0)		 2	| 20 (Dd(C+4)A1)// 偏移与 B 中相同
28	| {vfptr}
32	| ma0						D::$vftable@A0@:	// 来自 A0 的虚函数
	+---						 0	| &D::{dtor}
	+--- (virtual base A1)		 1	| &D::f
36	| {vfptr}					 2	| &D::f2
40	| ma1
	+---						D::$vftable@A1@:	// 来自 A1 的虚函数
								 0	| &thunk: this-=8; goto D::{dtor}
								 1	| &D::g
								 2	| &D::g2

对各个类的内存布局有了大致的了解后,接下来来看一下类 D 的构造和析构的情况。

·构造

01392527  push  1						; 传入 1
01392529  mov   ecx,dword ptr [ebp-0D4h]  
0139252F  call  D::D (013911B8h)  

D::D:
....									; 省略函数入口代码
01391B2F  pop   ecx						; this 指针
01391B30  mov   dword ptr [this],ecx
01391B33  mov   dword ptr [ebp-0D4h],0
01391B3D  cmp   dword ptr [ebp+8],0		; 为 0 跳过,防止重复构造虚基类
01391B41  je    D::D+6Bh (01391B7Bh)
01391B43  mov   eax,dword ptr [this]	; 设置 vbtable_B
01391B46  mov   dword ptr [eax+4],offset D::`vbtable' (01397C20h)
01391B4D  mov   eax,dword ptr [this]	; 设置 vbtable_C
01391B50  mov   dword ptr [eax+10h],offset D::`vbtable' (01397C30h)
01391B57  mov   ecx,dword ptr [this]
01391B5A  add   ecx,1Ch					; 修正偏移后调用构造函数
01391B5D  call  A0::A0 (01391087h)
01391B62  or    dword ptr [ebp-0D4h],1
01391B69  mov   ecx,dword ptr [this]
01391B6C  add   ecx,24h
01391B6F  call  A1::A1 (0139143Dh)
01391B74  or    dword ptr [ebp-0D4h],2
01391B7B  push  0						; 传入 0,防止重复构造
01391B7D  mov   ecx,dword ptr [this]
01391B80  call  B::B (0139142Eh)
01391B85  push  0
01391B87  mov   ecx,dword ptr [this]
01391B8A  add   ecx,0Ch
01391B8D  call  C::C (01391082h)
01391B92  mov   eax,dword ptr [this]	; <开始 D 自身的构造> 设置 vftable_B
01391B95  mov   dword ptr [eax],offset D::`vftable' (01397BE4h)
01391B9B  mov   eax,dword ptr [this]	; 设置 vftable_C
01391B9E  mov   dword ptr [eax+0Ch],offset D::`vftable' (01397BF0h)
01391BA5  mov   eax,dword ptr [this]
01391BA8  mov   ecx,dword ptr [eax+4]
01391BAB  mov   edx,dword ptr [ecx+4]
01391BAE  mov   eax,dword ptr [this]	; 由 B 的 vbptr 取得 A0 的虚表指针并赋值
01391BB1  mov   dword ptr [eax+edx+4],offset D::`vftable' (01397BFCh)
01391BB9  mov   eax,dword ptr [this]
01391BBC  mov   ecx,dword ptr [eax+4]
01391BBF  mov   edx,dword ptr [ecx+8]
01391BC2  mov   eax,dword ptr [this]	; 由 B 的 vbptr 取得 A1 的虚表指针并赋值
01391BC5  mov   dword ptr [eax+edx+4],offset D::`vftable' (01397C10h)
01391BCD  mov   eax,dword ptr [this]	; 返回 this
....									; 省略函数出口代码

可以看到,D 中保存了 4 个类各自的虚表,有了这些虚表,可以方便的将 D 类转换为对应的父类指针,而且在转为父类时,子类继承自其它父类的虚函数被屏蔽,无法调用。
另外在调用 D 的构造函数中传入了一个隐含的标志参数,用来决定是否进行虚基类的构造,以此避免重复构造的问题。由此也可以知道,类中的虚表指针,以及类的虚表并不是唯一的,需要确认它的继承体系。如果 D 有非继承的虚函数,那么它将会被记录到 D 的 vftable_B 中。

·析构

D::`vector deleting destructor':
....									; 省略函数入口代码
01342010  mov   dword ptr [this],ecx  
01342013  mov   ecx,dword ptr [this]  
01342016  sub   ecx,1Ch					; 将 this 从 A0 调整为 D
01342019  call  D::`vbase destructor' (013410D7h)  
0134201E  mov   eax,dword ptr [ebp+8]  
01342021  and   eax,1  
01342024  je    D::`scalar deleting destructor'+47h (01342037h)  
01342026  push  2Ch  
01342028  mov   eax,dword ptr [this]  
0134202B  sub   eax,1Ch  
0134202E  push  eax  
0134202F  call  operator delete (01341055h)  
01342034  add   esp,8  
01342037  mov   eax,dword ptr [this]
....									; 省略函数出口代码

D::`vbase destructor':
....									; 省略函数入口代码
01341DA0  mov   dword ptr [this],ecx  
01341DA3  mov   ecx,dword ptr [this]  
01341DA6  add   ecx,1Ch					; this 改为 A0
01341DA9  call  D::~D (0134146Ah)  
01341DAE  mov   ecx,dword ptr [this]  
01341DB1  add   ecx,24h  
01341DB4  call  A1::~A1 (013410A0h)  
01341DB9  mov   ecx,dword ptr [this]  
01341DBC  add   ecx,1Ch  
01341DBF  call  A0::~A0 (01341456h)
....									; 省略函数出口代码

D::~D:
....									; 省略函数入口代码
01341D40  mov   dword ptr [this],ecx  
01341D43  mov   ecx,dword ptr [this]  
01341D46  sub   ecx,4  
01341D49  call  C::~C (0134132Ah)  
01341D4E  mov   ecx,dword ptr [this]  
01341D51  sub   ecx,10h  
01341D54  call  B::~B (013411D6h)
....									; 省略函数出口代码

析构过程中直接将虚基类的调用放在了最后,依次执行父类的析构之后,再调用虚基类的析构。而不是将其包含在父类中,再像构造的时候用标志位区分是否构造。自然,B、C 的析构函数就不再含有对 AX::~AX 的调用了。

有趣的是,当给 C 加上析构函数时,类的布局会发生变化。编译器会分别在原先的基础上,在 A0、A1 前加上 4 字节的 vtordisp 填充,变为

1>	class D size(52):
1>		+---
1>	 0	| +--- (base class B)
1>	 0	| | {vfptr}
1>	 4	| | {vbptr}
1>	 8	| | mb
1>		| +---
1>	12	| +--- (base class C)
1>	12	| | {vfptr}
1>	16	| | {vbptr}
1>	20	| | mc
1>		| +---
1>	24	| md
1>		+---
1>	28	| (vtordisp for vbase A0)	<---
1>		+--- (virtual base A0)
1>	32	| {vfptr}
1>	36	| ma0
1>		+---
1>	40	| (vtordisp for vbase A1)	<---
1>		+--- (virtual base A1)
1>	44	| {vfptr}
1>	48	| ma1
1>		+---

查看了一下似乎是在析构函数中,具体来说是在 D::~D 中(此时的 this 为 A0)调用 C::~C 前将 ecx 设置为 vtordisp for vbase A0,随后将其视为 this 进行 vptr 以及成员的定位。

微软的解释是重写了虚函数并在构造/析构函数里调用了重写的函数时会插入这个字段,定义了 D(或 B、C)的析构函数满足了这个要求,而编译器自动生成的析构函数似乎不算在内。