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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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的基础语法。 下面的操…