当无法执行RVO时,是否需要显式编写std::move?
咱们先从最基础的共识说起:绝对不要对单个局部命名对象的直接返回加std::move,因为这会直接阻止编译器做RVO(返回值优化)——这可是编译器能给你做的最划算的性能优化之一,能直接跳过复制/移动构造,把对象直接构造在目标内存里。
那回到你的核心问题:当确定RVO完全没法执行的时候,该怎么处理?我们结合C标准和《Effective Modern C》的内容来拆解清楚:
先看标准和权威书籍的规则
C++14的两阶段重载解析逻辑
当满足复制/移动操作的省略条件(但不适用于异常声明),且待复制对象由左值指定,或者return语句中的表达式是命名了最内层封闭函数或lambda表达式的函数体或参数声明子句中声明的自动存储期对象的(可能带括号的)id表达式时,选择复制构造函数的重载解析首先会将该对象当作右值来执行。如果第一次重载解析失败或未执行,或者所选构造函数的第一个参数不是该对象类型的右值引用(可能带cv限定),则会再次执行重载解析,将该对象当作左值考虑。
[注:无论是否会发生复制省略,都必须执行这种两阶段重载解析。它确定了当省略未执行时要调用的构造函数,且即使调用被省略,所选构造函数也必须可访问。——结束注]
简单翻译一下:如果你的return语句符合RVO的触发条件(比如直接返回一个局部命名对象),哪怕编译器最终没做RVO,它也会先尝试把这个对象当作右值来匹配构造函数——相当于自动帮你做了std::move的效果。
《Effective Modern C++》的补充说明
这本书里把这个规则说得更直白:只要满足RVO的条件,不管编译器最终做不做优化,要么直接省略复制/移动,要么就必须把返回的局部对象当作右值处理——也就是标准强制要求了“隐式std::move”的兜底行为。
你的示例代码到底发生了什么?
先修正一下你代码里的小笔误(移动构造的参数应该是Test&&,不是const Test&&),然后看这段代码:
#include <iostream> #include <string> struct Test { Test() {} Test(const Test& other) { std::cout << "Test(const Test&)" << std::endl; } Test(Test&& other) { std::cout << "Test(Test&&)" << std::endl; } }; Test foo(int param) { Test test1; Test test2; return param > 5 ? std::move(test1) : test2; } int main() { Test res = foo(2); }
你看到输出是Test(const Test&),原因很简单:这个return语句的表达式是条件运算符的结果,它完全不符合RVO的触发条件——RVO要求return的是单个的、直接命名的局部对象,而条件表达式的结果是一个“二选一”的表达式,编译器根本没法做RVO。
这时候,标准里的“隐式std::move”规则就不生效了:当param=2时,返回的是test2,它是一个左值,所以编译器只能调用复制构造函数;如果param>5,你显式加了std::move(test1),它变成右值,就会调用移动构造函数。
最终结论:什么时候该显式加std::move?
答案是:只有当你确定RVO完全无法触发的时候,才需要显式加std::move,分两种场景:
- 可以触发RVO的场景:比如
return test;这种直接返回单个局部命名对象的情况,哪怕你觉得编译器可能不做优化,也别加std::move——标准已经兜底了,要么RVO,要么隐式转右值,你加了反而会直接阻止RVO。 - 无法触发RVO的场景:比如你的示例里用条件运算符返回两个对象之一,或者返回的是函数调用的结果、或者是某个表达式计算后的对象,这时候RVO完全没机会生效,显式给要返回的局部对象加
std::move,就能触发移动构造,避免不必要的深拷贝。
内容的提问来源于stack exchange,提问作者Ashot

