函数通过不同路径返回局部变量与临时对象时的Copy elision问题
C++ Copy Elision 实际触发条件解析
定义的Foo类
class Foo { public: Foo(std::string s) : s_(std::move(s)) { std::cout << "Constructor " << s_ << "\n"; } ~Foo() { std::cout << "Destructor " << s_ << "\n"; } Foo(const Foo& other) { s_ = other.s_; std::cout << "Copy Constructor\n"; } Foo& operator=(const Foo& other) { std::cout << "Copy assignment\n"; s_ = other.s_; return *this; } Foo(Foo&& other) { s_ = std::move(other.s_); std::cout << "Move Constructor\n"; } Foo& operator=(Foo&& other) { std::cout << "Move assignment\n"; s_ = std::move(other.s_); return *this; } void Set(std::string s) { s_ = std::move(s); } const std::string& Get() const { return s_; } private: std::string s_; };
预期触发Copy Elision的常规场景
该场景触发具名返回值优化(NRVO),直接在ret的内存位置构造foo,避免移动/拷贝:
Foo DoSomething(std::string s) { Foo foo{std::move(s)}; if (foo.Get() == "2") { return foo; } if (foo.Get() == "4") { return foo; } foo.Set("1"); return foo; } int main() { Foo ret(DoSomething("5")); }
困惑的场景分析
场景1
Foo DoSomething(std::string s) { Foo foo{std::move(s)}; if (foo.Get() == "2") { return foo; } if (foo.Get() == "4") { return Foo("4"); } foo.Set("1"); return foo; } int main() { Foo ret(DoSomething("5")); }
结果:调用移动构造函数,未触发Copy Elision。
场景2
Foo DoSomething(std::string s) { if (s == "2") { return Foo("2"); } if (s == "4") { return Foo("4"); } Foo foo{std::move(s)}; foo.Set("1"); return foo; } int main() { Foo ret(DoSomething("5")); }
结果:触发Copy Elision。
场景3
Foo DoSomething(std::string s) { Foo foo1{"2"}; Foo foo2{"4"}; if (s == "2") { return foo1; } if (s == "4") { return foo2; } Foo foo{std::move(s)}; return foo; } int main() { Foo ret(DoSomething("4")); }
结果:未触发Copy Elision。
场景4
Foo DoSomething(std::string s) { Foo foo1{"2"}; Foo foo2{"4"}; if (s == "2") { return foo1; } if (s == "4") { return foo2; } Foo foo{std::move(s)}; return foo; } int main() { Foo ret(DoSomething("5")); }
结果:触发Copy Elision。
实际触发Copy Elision的核心逻辑
先明确两种核心优化类型:
- RVO(返回值优化):返回临时对象时,编译器直接在目标位置构造对象,C++17起强制保证,所有场景下只要返回临时对象都会触发。
- NRVO(具名返回值优化):返回函数内的具名局部变量时,编译器优化掉移动/拷贝。C++20在满足条件时强制触发,实际判断逻辑的关键要求如下:
针对各场景逐一解析:
- 场景1未触发:函数存在两种不同返回分支——一个返回具名变量
foo,另一个返回临时对象。编译器无法为所有返回路径复用同一内存位置,因此返回foo时只能走移动构造。 - 场景2触发:传入"5"时,执行路径仅会走到返回
foo的分支。编译器可将foo直接构造在ret的内存位置,从而触发NRVO。 - 场景3未触发:传入"4"时返回
foo2,但foo1、foo2、foo的生命周期完全重叠,编译器无法将三个变量放在同一返回位置,只能通过移动构造返回foo2。 - 场景4触发:传入"5"时返回
foo,foo1和foo2虽存在但不会被当前路径返回。编译器可将foo直接构造在ret的内存位置,触发NRVO。
总结NRVO的实际触发条件(C++20):
- 函数返回路径要么返回同一个具名局部变量,要么返回临时对象(RVO);
- 返回的具名变量是当前执行路径下唯一的返回对象,且生命周期未被其他返回分支的变量干扰;
- 具名变量必须是函数内的非静态局部变量,不能是参数或静态变量。
内容的提问来源于stack exchange,提问作者Bruce
相关产品推荐
相关产品推荐

