刷到一篇文章:
作者:

原文:虛擬繼承的邪惡
讨论到这样的一个程序,最终输出什么???
代码有简化命名
using namespace std;
class A
{
public:
A(int a = 0) : v(a) {};
int v;
};
template <typename T>
class B : public virtual A
{
};
class C : public B<C>
{
public:
C(int a) : A(a) {};
};
class D : public C
{
public:
D(int a) : C(a) {};
};
int main()
{
cout << C(123).v << endl;
cout << D(456).v << endl;
return 0;
}
答案是:
123
0
是不是反直觉?为什么D(456)
变成了0
???
原文给出的问题答案:
事實上,上面的行為完全符合 C++ 標準的定義。簡單講,C++ 在初始化物件的時候,有下面幾個步驟:
1、按 depth-first traversal 對每一個 virtual base class 初始化一次。
2、依序初始化所有的 direct non-virtual base classes。
接下来看看代码到底是怎么生成的:
A(int a = 0) : v(a) {};
这一行,没有任务问题,很常规的构造函数,赋值
0000000000401232 <A::A(int)>:
A(int a = 0) : v(a) {};
401232: 55 push %rbp
401233: 48 89 e5 mov %rsp,%rbp
401236: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40123a: 89 75 f4 mov %esi,-0xc(%rbp)
40123d: 48 8b 45 f8 mov -0x8(%rbp),%rax
401241: 8b 55 f4 mov -0xc(%rbp),%edx
401244: 89 10 mov %edx,(%rax)
401246: 90 nop
401247: 5d pop %rbp
401248: c3 ret
401249: 90 nop
B::B()
默认生成的构造函数,奇怪,为什么没有调用A::A
呢?
因为,在这个用例中,B类从来没有被实例化过,B只是一个继承关系中的传宗接代工具人
。
如果B没有实例化,那么,A是B的虚基类,A的构造就不需要B来实现,B只会调用B的非虚基类构造函数,此例中,B没有非虚父类
000000000040124a <B<C>::B()>:
class B : public virtual A {};
40124a: 55 push %rbp
40124b: 48 89 e5 mov %rsp,%rbp
40124e: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this
401252: 48 89 75 f0 mov %rsi,-0x10(%rbp) # 指针
401256: 48 8b 45 f0 mov -0x10(%rbp),%rax
40125a: 48 8b 10 mov (%rax),%rdx
40125d: 48 8b 45 f8 mov -0x8(%rbp),%rax
401261: 48 89 10 mov %rdx,(%rax) # 指针里的内容 存到了 this的位置
401264: 90 nop
401265: 5d pop %rbp
401266: c3 ret
401267: 90 nop
接下来,看到了两个C::C(int)
构造函数
为什么会出现两个C::C(int)
呢,因为C直接实例化时一个,C被当作传宗接代工具人
时另一个。一个需要调用虚基类构造,一个不能。
先看第一个:
这个没有调用虚基类A的构造函数,所以,这个是D使用的基类C的构造函数,当D
实例化时,A
由D
负责实例化,所以C这里不会调用A的构造函数,虽然代码里写了。。。C(int a) : A(a) {};
,在这里,A(a)
从来没有用到过,不会执行。
%edx,-0x14(%rbp)
,dx
寄存器是第三个传参,即(int a)
,保存到的-0x14(%rbp)
地址从没使用过。A(a)
没有发生
0000000000401268 <C::C(int)>:
C(int a) : A(a) {};
401268: 55 push %rbp
401269: 48 89 e5 mov %rsp,%rbp
40126c: 48 83 ec 20 sub $0x20,%rsp
401270: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this
401274: 48 89 75 f0 mov %rsi,-0x10(%rbp) # 还是一个指针
401278: 89 55 ec mov %edx,-0x14(%rbp) # int a 后面没有用到
40127b: 48 8b 45 f8 mov -0x8(%rbp),%rax
40127f: 48 8b 55 f0 mov -0x10(%rbp),%rdx
401283: 48 83 c2 08 add $0x8,%rdx
401287: 48 89 d6 mov %rdx,%rsi
40128a: 48 89 c7 mov %rax,%rdi
40128d: e8 b8 ff ff ff call 40124a <B<C>::B()> # B::B(this, 指针 + 8)
401292: 48 8b 45 f0 mov -0x10(%rbp),%rax
401296: 48 8b 10 mov (%rax),%rdx
401299: 48 8b 45 f8 mov -0x8(%rbp),%rax
40129d: 48 89 10 mov %rdx,(%rax) # 指针里的内容 存到了 this的位置
4012a0: 90 nop
4012a1: c9 leave
4012a2: c3 ret
4012a3: 90 nop
这一段,是C被实例化时的构造,会调用A::A
,对应cout << C(123).v << endl;
这一段代码,很符合自觉,int a
的值被传下去了,并且首先构造了A,然后编译器又帮我我们自动调用了B的构造。
00000000004012a4 <C::C(int)>:
4012a4: 55 push %rbp
4012a5: 48 89 e5 mov %rsp,%rbp
4012a8: 48 83 ec 10 sub $0x10,%rsp
4012ac: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this
4012b0: 89 75 f4 mov %esi,-0xc(%rbp) # int a
4012b3: 48 8b 45 f8 mov -0x8(%rbp),%rax
4012b7: 48 8d 50 08 lea 0x8(%rax),%rdx
4012bb: 8b 45 f4 mov -0xc(%rbp),%eax
4012be: 89 c6 mov %eax,%esi
4012c0: 48 89 d7 mov %rdx,%rdi
4012c3: e8 6a ff ff ff call 401232 <A::A(int)> # A::A(this + 8, a)
4012c8: 48 8b 45 f8 mov -0x8(%rbp),%rax
4012cc: ba 88 20 40 00 mov $0x402088,%edx
4012d1: 48 89 d6 mov %rdx,%rsi
4012d4: 48 89 c7 mov %rax,%rdi
4012d7: e8 6e ff ff ff call 40124a <B<C>::B()> # B::B(this, $0x402088)
4012dc: ba 80 20 40 00 mov $0x402080,%edx
4012e1: 48 8b 45 f8 mov -0x8(%rbp),%rax
4012e5: 48 89 10 mov %rdx,(%rax) # *this = $0x402080
4012e8: 90 nop
4012e9: c9 leave
4012ea: c3 ret
4012eb: 90 nop
D类同理,D被实例化,先构造虚基类A
但是D(int a) : C(a) {};
,我们自己写了D的构造函数,但没有按照C++规范在虚继承的最后的派生类中构造虚基类,所以,很不幸的是这里编译器帮我们构造了虚基类A,调用了虚基类的默认构造或有默认值的构造函数A::A(int a = 0)
,这里是按a=0
进行了A的构造,编译器没有向我们发出警告。。。
然后,由于虚基类应该由我们构造,我们没指定,进行了默认构造
接下来是非虚基类的构造,C
的构造,由于C不是A-B-C-D
这一链条中的最后的派生类,C
不会构造A
(还记得吗,编译器生成了两个C::C
),B
也不会构造A
(编译器生成的B::B只有一个,非最远派生类的构造函数,不会构造A
)
所以D(int a) : C(a) {};
并没有按照我们预期的工作。。。
00000000004012ec <D::D(int)>:
D(int a) : C(a) {};
4012ec: 55 push %rbp
4012ed: 48 89 e5 mov %rsp,%rbp
4012f0: 48 83 ec 10 sub $0x10,%rsp
4012f4: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this
4012f8: 89 75 f4 mov %esi,-0xc(%rbp) # int a
4012fb: 48 8b 45 f8 mov -0x8(%rbp),%rax
4012ff: 48 83 c0 08 add $0x8,%rax
401303: be 00 00 00 00 mov $0x0,%esi
401308: 48 89 c7 mov %rax,%rdi
40130b: e8 22 ff ff ff call 401232 <A::A(int)> # A::A(this + 8, 0),编译器帮我们构造了A,但用了默认构造或有默认值的构造函数
401310: 48 8b 45 f8 mov -0x8(%rbp),%rax
401314: b9 28 20 40 00 mov $0x402028,%ecx
401319: 8b 55 f4 mov -0xc(%rbp),%edx
40131c: 48 89 ce mov %rcx,%rsi
40131f: 48 89 c7 mov %rax,%rdi
401322: e8 41 ff ff ff call 401268 <C::C(int)> # C::C(this, $0x402028, a),但这里调用的C::C构造不会用到 a
401327: ba 20 20 40 00 mov $0x402020,%edx
40132c: 48 8b 45 f8 mov -0x8(%rbp),%rax
401330: 48 89 10 mov %rdx,(%rax) # *this = $0x402020
401333: 90 nop
401334: c9 leave
401335: c3 ret
最后回头看main
,一切都很平常,正常调用C::C
和D::D
,只是调用C::C
时候,C
写了构造A
,而调用D::D
之后没写构造A
,追踪造成第一次输出正常,第二输出了0
int main()
{
401176: 55 push %rbp
401177: 48 89 e5 mov %rsp,%rbp
40117a: 48 83 ec 20 sub $0x20,%rsp
cout << C(123).v << endl;
40117e: 48 8d 45 e0 lea -0x20(%rbp),%rax
401182: be 7b 00 00 00 mov $0x7b,%esi
401187: 48 89 c7 mov %rax,%rdi
40118a: e8 15 01 00 00 call 4012a4 <C::C(int)> # C::C(-20(bp), 123)
40118f: 8b 45 e8 mov -0x18(%rbp),%eax
401192: 89 c6 mov %eax,%esi
401194: bf 40 40 40 00 mov $0x404040,%edi
401199: e8 d2 fe ff ff call 401070 <std::basic_ostream<char, std::char_traits<char> >::operator<<(int)@plt> # <<(cout, -18(rbp)) 此时,c->v 直接获得地址
40119e: be 30 10 40 00 mov $0x401030,%esi
4011a3: 48 89 c7 mov %rax,%rdi
4011a6: e8 a5 fe ff ff call 401050 <std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))@plt>
cout << D(456).v << endl;
4011ab: 48 8d 45 f0 lea -0x10(%rbp),%rax
4011af: be c8 01 00 00 mov $0x1c8,%esi
4011b4: 48 89 c7 mov %rax,%rdi
4011b7: e8 30 01 00 00 call 4012ec <D::D(int)> # D::D(-10(bp), 456)
4011bc: 8b 45 f8 mov -0x8(%rbp),%eax
4011bf: 89 c6 mov %eax,%esi
4011c1: bf 40 40 40 00 mov $0x404040,%edi
4011c6: e8 a5 fe ff ff call 401070 <std::basic_ostream<char, std::char_traits<char> >::operator<<(int)@plt> # <<(cout, -8(rbp)) 此时,d->v 直接获得地址
4011cb: be 30 10 40 00 mov $0x401030,%esi
4011d0: 48 89 c7 mov %rax,%rdi
4011d3: e8 78 fe ff ff call 401050 <std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))@plt>
return 0;
4011d8: b8 00 00 00 00 mov $0x0,%eax
4011dd: c9 leave
4011de: c3 ret
原作者这段写的一点没错,不过通过汇编的角度观察,我们知道了编译器是如何实现的:
虚继承的派生类构造函数,编译器为一个构造函数生成了两个实现,分别是这个派生类直接实例化时的构造韩慧,会先构造虚基类,一个是作为其他类的基类时,不会构造虚基类,虚基类由其最后的派生类负责构造。
如果派生类忘记构造虚基类,编译器会帮助我们进行执行基类的默认构造或有默认值的构造,而这,没有警告,悄悄发生。
事實上,上面的行為完全符合 C++ 標準的定義。簡單講,C++ 在初始化物件的時候,有下面幾個步驟:
1、按 depth-first traversal 對每一個 virtual base class 初始化一次。
2、依序初始化所有的 direct non-virtual base classes。
最后,补一个作者提到的google c++ 规范
眾所周知,虛擬繼承是為了解決多重繼承產生的 diamond problem 而來的概念。大概是因為虛擬繼承有這些不為人知的眉角,Google C++ Style Guide 才會明定如果要多重繼承,所有的 direct base class 都得是純粹的 interface 而不能帶有成員變數。
c++
魔法无比强大。。。