为何C++编译器无法优化单次使用局部变量的额外拷贝?
C++ 传参拷贝优化差异测试与说明
测试环境
所有测试用例均在x86_64-pc-linux-gnu平台下使用g++ (GCC) 12.1.0编译,使用的编译指令为:
g++ -O3 -std=gnu++20 main.cpp
示例1代码
#include <iostream> class Foo { public: Foo(){ std::cout << "Foo constructor" << std::endl;} Foo(Foo&& f) { std::cout << "Foo move constructor" << std::endl;} Foo(const Foo& f) {std::cout << "Foo copy constructor" << std::endl;} ~Foo() { std::cout << "Foo destructor" << std::endl;} }; class Baz { private: Foo foo; public: Baz(Foo foo) : foo(std::move(foo)) {} ~Baz() { std::cout << "Baz destructor" << std::endl;} }; Foo bar() { return Foo(); } int main() { Baz baz(bar()); return 0; }
运行输出
Foo constructor Foo move constructor Foo destructor Baz destructor Foo destructor
示例2代码
#include <iostream> class Foo { public: Foo(){ std::cout << "Foo constructor" << std::endl;} Foo(Foo&& f) { std::cout << "Foo move constructor" << std::endl;} Foo(const Foo& f) {std::cout << "Foo copy constructor" << std::endl;} ~Foo() { std::cout << "Foo destructor" << std::endl;} }; class Baz { private: Foo foo; public: Baz(Foo foo) : foo(std::move(foo)) {} ~Baz() { std::cout << "Baz destructor" << std::endl;} }; Foo bar() { return Foo(); } int main() { // 唯一差异点 Foo f = bar(); Baz baz(f); return 0; }
运行输出
Foo constructor Foo copy constructor Foo move constructor Foo destructor Baz destructor Foo destructor Foo destructor
差异原因说明
两段代码逻辑本质上并不等价,出现额外拷贝构造是C++标准语义下的必然结果,和编译器优化力度无关,核心原因有三点:
- 左右值语义决定了构造方式的区别
示例1中传给Baz构造函数的是bar()返回的纯右值,按值传参时直接在形参内存位置构造临时对象,C++17标准强制要求这种场景下的*返回值优化(RVO)*生效,不会产生额外的拷贝/移动。之后形参foo是有名字的左值,用std::move转成右值后移动构造Baz的成员变量,全程只触发一次移动构造,没有拷贝。
示例2中显式定义了栈上命名变量f,f是左值。传给Baz构造函数时,需要用左值f构造按值传入的形参,左值初始化对象会优先调用拷贝构造,这就是多出来的那次拷贝构造的直接来源。 - 编译器优化不能突破as-if规则约束
编译器所有优化的前提是不能改变程序的可观测行为。示例2中f是显式声明的命名变量,生命周期从声明点持续到main函数结束,且Foo的拷贝构造、析构函数都有可观测的副作用(打印日志),编译器无权擅自消除f这个变量、跳过拷贝构造调用,否则程序输出就会和标准要求的语义不一致,属于违规优化。 - 拷贝消除有严格的适用场景限制
C++标准允许的拷贝消除(也就是常说的RVO/NRVO优化)只覆盖纯右值传参、返回临时对象等少数特定场景,命名左值变量不在允许优化的范围内,只要你把临时值赋值给了一个有名字的变量,后续用这个变量传参就必须按左值语义走拷贝或者移动,没有例外。
如果确实想把临时值存到命名变量里同时避免拷贝,传参时手动加std::move转成右值即可,也就是把示例2里的Baz baz(f);改成Baz baz(std::move(f));,只要确认后续代码不会再使用f,这种写法是安全的,生成的代码和示例1几乎一致。
内容的提问来源于stack exchange,提问作者Cheiron
相关产品推荐
相关产品推荐

