刷到一篇文章:
作者:
原文:虛擬繼承的邪惡
討論到這樣的一個程序,最終輸出什么???
代碼有簡化命名
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 %rbp401233: 48 89 e5 mov %rsp,%rbp401236: 48 89 7d f8 mov %rdi,-0x8(%rbp)40123a: 89 75 f4 mov %esi,-0xc(%rbp)40123d: 48 8b 45 f8 mov -0x8(%rbp),%rax401241: 8b 55 f4 mov -0xc(%rbp),%edx401244: 89 10 mov %edx,(%rax)401246: 90 nop401247: 5d pop %rbp401248: c3 ret401249: 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 %rbp40124b: 48 89 e5 mov %rsp,%rbp40124e: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this401252: 48 89 75 f0 mov %rsi,-0x10(%rbp) # 指針401256: 48 8b 45 f0 mov -0x10(%rbp),%rax40125a: 48 8b 10 mov (%rax),%rdx40125d: 48 8b 45 f8 mov -0x8(%rbp),%rax401261: 48 89 10 mov %rdx,(%rax) # 指針里的內容 存到了 this的位置401264: 90 nop401265: 5d pop %rbp401266: c3 ret401267: 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 %rbp401269: 48 89 e5 mov %rsp,%rbp40126c: 48 83 ec 20 sub $0x20,%rsp401270: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this401274: 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),%rax40127f: 48 8b 55 f0 mov -0x10(%rbp),%rdx401283: 48 83 c2 08 add $0x8,%rdx401287: 48 89 d6 mov %rdx,%rsi40128a: 48 89 c7 mov %rax,%rdi40128d: e8 b8 ff ff ff call 40124a <B<C>::B()> # B::B(this, 指針 + 8)401292: 48 8b 45 f0 mov -0x10(%rbp),%rax401296: 48 8b 10 mov (%rax),%rdx401299: 48 8b 45 f8 mov -0x8(%rbp),%rax40129d: 48 89 10 mov %rdx,(%rax) # 指針里的內容 存到了 this的位置4012a0: 90 nop4012a1: c9 leave4012a2: c3 ret4012a3: 90 nop
這一段,是C被實例化時的構造,會調用A::A
,對應cout << C(123).v << endl;
這一段代碼,很符合自覺,int a
的值被傳下去了,并且首先構造了A,然后編譯器又幫我我們自動調用了B的構造。
00000000004012a4 <C::C(int)>:4012a4: 55 push %rbp4012a5: 48 89 e5 mov %rsp,%rbp4012a8: 48 83 ec 10 sub $0x10,%rsp4012ac: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this4012b0: 89 75 f4 mov %esi,-0xc(%rbp) # int a4012b3: 48 8b 45 f8 mov -0x8(%rbp),%rax4012b7: 48 8d 50 08 lea 0x8(%rax),%rdx4012bb: 8b 45 f4 mov -0xc(%rbp),%eax4012be: 89 c6 mov %eax,%esi4012c0: 48 89 d7 mov %rdx,%rdi4012c3: e8 6a ff ff ff call 401232 <A::A(int)> # A::A(this + 8, a)4012c8: 48 8b 45 f8 mov -0x8(%rbp),%rax4012cc: ba 88 20 40 00 mov $0x402088,%edx4012d1: 48 89 d6 mov %rdx,%rsi4012d4: 48 89 c7 mov %rax,%rdi4012d7: e8 6e ff ff ff call 40124a <B<C>::B()> # B::B(this, $0x402088)4012dc: ba 80 20 40 00 mov $0x402080,%edx4012e1: 48 8b 45 f8 mov -0x8(%rbp),%rax4012e5: 48 89 10 mov %rdx,(%rax) # *this = $0x4020804012e8: 90 nop4012e9: c9 leave4012ea: c3 ret4012eb: 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 %rbp4012ed: 48 89 e5 mov %rsp,%rbp4012f0: 48 83 ec 10 sub $0x10,%rsp4012f4: 48 89 7d f8 mov %rdi,-0x8(%rbp) # this4012f8: 89 75 f4 mov %esi,-0xc(%rbp) # int a4012fb: 48 8b 45 f8 mov -0x8(%rbp),%rax4012ff: 48 83 c0 08 add $0x8,%rax401303: be 00 00 00 00 mov $0x0,%esi401308: 48 89 c7 mov %rax,%rdi40130b: e8 22 ff ff ff call 401232 <A::A(int)> # A::A(this + 8, 0),編譯器幫我們構造了A,但用了默認構造或有默認值的構造函數401310: 48 8b 45 f8 mov -0x8(%rbp),%rax401314: b9 28 20 40 00 mov $0x402028,%ecx401319: 8b 55 f4 mov -0xc(%rbp),%edx40131c: 48 89 ce mov %rcx,%rsi40131f: 48 89 c7 mov %rax,%rdi401322: e8 41 ff ff ff call 401268 <C::C(int)> # C::C(this, $0x402028, a),但這里調用的C::C構造不會用到 a401327: ba 20 20 40 00 mov $0x402020,%edx40132c: 48 8b 45 f8 mov -0x8(%rbp),%rax401330: 48 89 10 mov %rdx,(%rax) # *this = $0x402020401333: 90 nop401334: c9 leave401335: c3 ret
最后回頭看main
,一切都很平常,正常調用C::C
和D::D
,只是調用C::C
時候,C
寫了構造A
,而調用D::D
之后沒寫構造A
,追蹤造成第一次輸出正常,第二輸出了0
int main()
{401176: 55 push %rbp401177: 48 89 e5 mov %rsp,%rbp40117a: 48 83 ec 20 sub $0x20,%rspcout << C(123).v << endl;40117e: 48 8d 45 e0 lea -0x20(%rbp),%rax401182: be 7b 00 00 00 mov $0x7b,%esi401187: 48 89 c7 mov %rax,%rdi40118a: e8 15 01 00 00 call 4012a4 <C::C(int)> # C::C(-20(bp), 123)40118f: 8b 45 e8 mov -0x18(%rbp),%eax401192: 89 c6 mov %eax,%esi401194: bf 40 40 40 00 mov $0x404040,%edi401199: 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,%esi4011a3: 48 89 c7 mov %rax,%rdi4011a6: 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),%rax4011af: be c8 01 00 00 mov $0x1c8,%esi4011b4: 48 89 c7 mov %rax,%rdi4011b7: e8 30 01 00 00 call 4012ec <D::D(int)> # D::D(-10(bp), 456)4011bc: 8b 45 f8 mov -0x8(%rbp),%eax4011bf: 89 c6 mov %eax,%esi4011c1: bf 40 40 40 00 mov $0x404040,%edi4011c6: 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,%esi4011d0: 48 89 c7 mov %rax,%rdi4011d3: 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,%eax4011dd: c9 leave4011de: 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++
魔法無比強大。。。