拷贝/移动省略需显式定义拷贝/移动构造函数的技术问询
拷贝/移动省略与构造函数定义的关系分析
这个问题正好戳中了C++拷贝省略机制里一个很容易混淆的细节——很多人以为只要有拷贝/移动构造函数就能触发省略,其实构造函数的声明方式会直接影响编译器的优化逻辑。让我一步步拆解你的代码和背后的原理:
先明确核心前提:拷贝省略的“入场券”
根据C++标准,编译器可以在特定场景(比如返回值优化RVO、复制初始化省略)跳过拷贝/移动构造函数的调用,但有一个硬性要求:被省略的那个拷贝/移动构造函数必须是可访问且未被删除的。简单说就是:编译器优化的是“本应合法发生的拷贝/移动”,如果这个操作在语法上根本不成立(比如构造函数不存在、被删了),编译器连优化的资格都没有,直接报错。
1. 显式自定义拷贝/移动构造函数(带printf的版本)
看你注释里的这两个构造函数:
T(const T&) { printf("copy-constructed\n"); } T(T&&) { printf("move-constructed\n"); }
当你打开这两个注释时:
- 它们是
public的,完全满足“可访问、未被删除”的条件,给了编译器优化的入场券。 - 编译器会直接触发返回值优化(RVO):它不会在
f()里创建临时对象再拷贝/移动到main()的x里,而是直接在x的内存地址上构造那个临时对象。所以你会看到两次打印的地址完全一致,而且不会输出copy-constructed或move-constructed——因为拷贝/移动操作被彻底跳过了。 - 这里的逻辑是:你显式告诉编译器“这个拷贝/移动操作是合法的”,编译器就敢放心地把临时对象的构造地点直接替换成
x的位置,省去中间步骤。
2. 默认生成的拷贝/移动构造函数(= default版本)
你说= default的版本“does not help”,其实这是个误解,得仔细说:
T(const T&) = default; T(T&&) = default;
- 首先,
= default声明的构造函数是完全合法的(可访问、未被删除),满足拷贝省略的前提,编译器仍然会触发优化。那为什么你觉得没效果? - 问题出在平凡构造函数:
= default让编译器生成的是“平凡”的构造函数——这类构造函数没有任何用户自定义逻辑(比如你的printf),所以即使原本要调用它,也不会有日志输出。但你看地址的话,两次打印的地址肯定是相同的,说明RVO已经生效了,只是没有日志给你反馈而已。 - 简单说:不是没帮助,是帮助了但没留下痕迹。
3. 不声明任何拷贝/移动构造函数(代码默认状态)
当你把所有构造函数都注释掉时:
- 根据C++标准:因为你已经定义了自定义的默认构造函数
T(),编译器不会自动生成移动构造函数,但会自动生成一个平凡的默认拷贝构造函数(可访问)。 - 此时
f()返回临时对象时,因为没有移动构造函数,编译器会 fallback 到拷贝构造函数。而这个默认生成的拷贝构造函数是合法的,所以编译器仍然能触发RVO,跳过拷贝操作——你运行代码会看到地址是相同的,就是最好的证明。 - 反过来,如果你把默认拷贝构造函数也删了(比如
T(const T&) = delete;),编译器会直接报错,因为没有合法的构造函数能完成初始化,更别说省略了。
关键结论
- 拷贝省略的核心是:编译器优化的是合法的拷贝/移动操作——必须有可访问且未被删除的对应构造函数,才谈得上优化。
- 显式自定义构造函数:不仅满足合法条件,还能通过日志直观验证优化是否生效(没日志=优化成了)。
= default构造函数:同样满足条件,优化会生效,但因为是平凡构造函数,没有自定义日志,容易让人误以为没效果。- 最后提一句:拷贝省略是编译器的可选优化,但在开启常规优化(比如
-O2)的情况下,GCC、Clang、MSVC这些主流编译器都会主动触发RVO这类省略。
内容的提问来源于stack exchange,提问作者Al Gebra
相关产品推荐
相关产品推荐

