C++,從匯編角度看《虛擬繼承的邪惡》

刷到一篇文章:

作者:

原文:虛擬繼承的邪惡

討論到這樣的一個程序,最終輸出什么???

代碼有簡化命名

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實例化時,AD負責實例化,所以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::CD::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++魔法無比強大。。。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/88638.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/88638.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/88638.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

多 Agent 強化學習實踐指南(一):CTDE PPO 在合作捕食者-獵物游戲中的應用詳解

我們來詳細講解如何在合作捕食者-獵物游戲中結合 PPO (Proximal Policy Optimization) 算法。我們將聚焦于 CTDE&#xff08;Centralized Training, Decentralized Execution&#xff0c;集中訓練、分散執行&#xff09; 模式&#xff0c;因為這是處理合作多 Agent 任務的常用且…

Web應用文件上傳安全設計指南

引言 在當今的Web應用中&#xff0c;文件上傳功能已成為基礎且必要的服務能力&#xff0c;但不當的設計可能帶來目錄遍歷、代碼注入、服務端資源耗盡等安全風險。本文從威脅模型、安全設計原則、技術實現三個維度&#xff0c;系統闡述安全文件上傳架構的設計要點。 一、威脅模型…

用 React Three Fiber 實現 3D 城市模型的擴散光圈特效

本文介紹了如何使用 React Three Fiber&#xff08;R3F&#xff09;和 Three.js 實現一個從中心向外擴散的光圈特效&#xff08;DiffuseAperture 組件&#xff09;&#xff0c;并將其集成到城市 3D 模型&#xff08;CityModel 組件&#xff09;中。該特效通過動態調整圓柱幾何體…

【牛客刷題】COUNT數字計數

文章目錄 一、題目介紹二、題解思路三、算法實現四、復雜度分析五 、關鍵步驟解析5.1 數字分解5.2 三種情況處理5.2.1 情況1: d < c u r d < cur d<cur(完整周期)5.2.2 情況2: d = c u r d = cur d=cur(混合周期)5.2.3 情況3: d > c u r d > cur d>cu…

AGV穿梭不“迷路”CCLinkIE轉Modbus TCP的銜接技巧

在AGV控制系統集成中&#xff0c;工程師常面臨一個現實難題&#xff1a;如何讓CCLinkIE總線與Modbus TCP設備實現高效通信&#xff1f;這種跨協議的連接需求&#xff0c;往往需要耗費大量時間調試。本文將通過實際案例解析&#xff0c;為制造行業工程師提供可復用的解決方案。【…

【代碼隨想錄】刷題筆記——哈希表篇

目錄 242. 有效的字母異位詞 349. 兩個數組的交集 202. 快樂數 1. 兩數之和 454. 四數相加 II 383. 贖金信 15. 三數之和 18. 四數之和 242. 有效的字母異位詞 思路 代碼 class Solution {public boolean isAnagram(String s, String t) {if (s.length() ! t.length()…

Python爬蟲實戰:研究messytables庫相關技術

1. 引言 在當今數字化時代,互聯網上存在著大量有價值的數據。然而,這些數據通常以不規則的格式存在,尤其是表格數據,可能包含復雜的表頭、合并單元格、不規則布局等問題。傳統的數據處理工具往往難以應對這些挑戰。 網絡爬蟲技術可以幫助我們從網頁上自動提取數據,而 mes…

Vue3的組件通信方式

通信方式適用層級數據流向復雜度Props/Emits父子組件單向/雙向★☆☆v-model父子組件雙向★☆☆Provide/Inject跨層級組件自上而下★★☆事件總線任意組件任意方向★★★Pinia/Vuex全局狀態任意方向★★☆Refs模板引用父子組件父→子★☆☆作用域插槽父子組件子→父★★☆Web W…

創客匠人:大健康創始人IP如何用“社會責任”構建品牌護城河

一、商業與責任的失衡困局部分大健康IP將利潤置于首位&#xff0c;甚至犧牲用戶利益&#xff0c;導致品牌形象脆弱。某保健品公司因夸大宣傳被曝光后&#xff0c;盡管銷量曾達千萬&#xff0c;卻因缺乏社會認同&#xff0c;一夜之間崩塌&#xff0c;證明沒有社會責任支撐的商業…

AI:機器人未來的形態是什么?

機器人未來的形態將受到技術進步、應用場景需求和社會接受度的綜合影響&#xff0c;以下是對未來機器人形態的預測&#xff0c;涵蓋技術趨勢、設計方向和應用場景&#xff1a; 1. 形態多樣化與通用化 人形機器人&#xff08;Humanoid Robots&#xff09;&#xff1a; 趨勢&…

創建 UIKit 項目教程

一、打開 XCode&#xff0c;選擇 iOS 下的 App&#xff0c;然后點 Next二、Interface 選擇 Storyboard&#xff0c;然后點 Next三、刪掉 Main.storyboard四、刪掉 SceneDelegate.swift五、AppDelegate.swift 只保留第一個函數六、在 AppDelegate.swift 文件里的 application 函…

防爬蟲君子協定 Robots.txt 文件

1.什么是robots.txt ? robots.txt是一個位于網站根目錄的文本文件,用于指導搜索引擎爬蟲如何訪問和抓取網站內容。它遵循特定的語法規則,是網站與爬蟲通信的重要工具。當搜索引擎訪問一個網站時,它首先會檢查該網站的根域下是否有一個叫做robots.txt的純文本文件。Robots.…

淺談 Python 中的 yield——生成器對象與函數調用的區別

我們來看這么一個例子&#xff1a; def greeter():name yield "你是誰&#xff1f;"yield f"你好&#xff0c;{name}"g greeter() print(next(g)) # → "你是誰&#xff1f;" print(g.send("張三")) # → "你好&#xf…

云端docker小知識

1、docker的三個關鍵概念image、container、dockerfile2、docker的container3、dockerfile4、docker制作image5、linux&#xff08;ubuntu&#xff09;安裝docker&#xff08;步驟1和4&#xff09;6、docker基本命令docker images 查看全部鏡像docker rmi -f 1e5f3c5b981a 刪除…

【Elasticsearch】昂貴算法與廉價算法

在 Elasticsearch 里&#xff0c;“昂貴”并不單指“CPU 時間”&#xff0c;而是綜合了 **CPU、內存、磁盤 I/O、網絡傳輸** 以及 **實現復雜度** 的代價。下面把常見“昂貴算法”拆開說&#xff1a;1. **高計算密度的文本算法** ? **match_phrase slop**&#xff08;帶跨距…

深度學習-多分類

?開頭摘要??&#xff1a; 本文將深入探討如何使用PyTorch實現基于Softmax回歸的MNIST手寫數字識別系統。從多分類問題的核心概念出發&#xff0c;詳細解析??One-Hot編碼??技術如何將類別標簽向量化&#xff0c;剖析??交叉熵損失函數??的數學原理及其在訓練中的優化機…

JVM 類加載過程

一、加載&#xff08;Loading&#xff09;目標&#xff1a;把字節碼文件&#xff08;.class&#xff09;“讀入 JVM”&#xff0c;生成類的 “半成品”&#xff08;Class 對象&#xff09;。Bootstrap ClassLoader&#xff08;啟動類加載器&#xff09;&#xff1a;負責加載 JV…

通俗范疇論13 雞與蛋的故事番外篇

通俗范疇論13 雞與蛋的故事番外篇 在上一篇中,我們得到了雞與蛋的Set局部小范疇如下: 雞與蛋 SetSetSet 局部小范疇 如上圖所示,每個雞來自于一個蛋,每個蛋來自于一只雞,如此循環,以至于無窮… 是的,假設雞與蛋兩個對象代表的集合,都是無窮集合,這個系統就沒有問題…

記錄跟隨recyclerview滑動的指示器

老早之前做的一個功能&#xff0c;橫向recyclerview滑動時&#xff0c;底部做跟隨滑動指示器。今天代碼不用了&#xff0c;記錄下代碼。<LinearLayoutandroid:layout_width"match_parent"android:layout_height"wrap_content"android:layout_marginTop&…

快速過一遍Python基礎語法

前言 本文章是深度學習的前導課&#xff0c;對有編程基礎的小伙伴更加的友好&#xff08;C、C&#xff09;&#xff0c;如果完全沒有學過任何一門編程語言也沒有關系&#xff0c;本文章不會涉及到晦澀難懂的原理&#xff0c;只是簡單的帶大家過一遍Python的基礎語法。 下面的操…