27. 为什么在返回局部变量时,不应该显式使用std::move?
这个问题,我几乎每次C++培训都会拿出来讲。因为它太容易踩坑了。
很多初学者学了移动语义后,觉得「移动比拷贝快,那我返回局部变量时手动加个std::move,岂不是更高效?」
嗯,想法很美好,现实很残酷。这么做不仅不会更快,反而可能让代码变慢。
编译器比你想象的聪明
先看一个最简单的例子:
std::vector<int> createVector() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v; // 情况A:直接返回
}
std::vector<int> createVectorMove() {
std::vector<int> v = {1, 2, 3, 4, 5};
return std::move(v); // 情况B:显式move后返回
}
你觉得哪个更快?
我告诉你,情况A和情况B在绝大多数现代编译器上,生成的代码是一样的。甚至情况A可能更好。
为什么会这样?
因为C++标准里有个叫拷贝省略(copy elision)的机制,具体到返回局部变量这个场景,它叫NRVO(Named Return Value Optimization)。
核心要点:当编译器看到你返回一个局部变量时,它有权直接在调用者的内存位置上构造这个对象,从而完全跳过拷贝或移动操作。
你想想看,如果编译器已经帮你把构造过程优化掉了,你手动加个std::move,反而是在帮倒忙。
显式std::move的副作用
我见过不少代码,在返回局部变量时加了std::move,结果反而触发了移动构造,而不是拷贝省略。移动构造虽然比拷贝构造快,但比「什么都不做」要慢啊。
来看一个更具体的例子:
struct HeavyObject {
HeavyObject() { /* 分配大量资源 */ }
HeavyObject(const HeavyObject&) { /* 深拷贝,很慢 */ }
HeavyObject(HeavyObject&&) noexcept { /* 移动,较快 */ }
~HeavyObject() { /* 释放资源 */ }
};
HeavyObject createWithMove() {
HeavyObject obj;
// 做一些操作...
return std::move(obj); // 强制使用移动构造
}
HeavyObject createWithoutMove() {
HeavyObject obj;
// 做一些操作...
return obj; // 编译器可能直接省略构造
}
在createWithoutMove中,编译器可能直接在调用者的栈帧上构造obj,连移动都不需要。而createWithMove中,std::move把obj转成了右值引用,编译器反而没法做拷贝省略了——它必须调用移动构造函数。
我曾经踩过的坑:有一次在优化一个性能敏感模块时,我看到同事在返回局部std::string时加了std::move。我问他为什么这么做,他说「为了效率」。结果我测了一下,加了std::move的版本比不加的慢了约15%。原因就是编译器本来可以做拷贝省略,但被std::move破坏了。
那什么时候该用std::move?
别误会,我不是说std::move没用。它很有用,但要用对地方。
我个人习惯在以下几种场景使用std::move:
- 将对象传递给函数,且不再需要原对象:比如
foo(std::move(myObj)); - 在类内部移动成员变量:比如
ptr_(std::move(other.ptr_)) - 在容器中插入临时对象:比如
vec.push_back(std::move(localObj));
但返回局部变量时,千万别用。
特殊情况:条件返回
有一种情况需要特别注意。如果你的函数中有多个返回路径,且返回的是同一个局部变量,编译器可能无法做NRVO:
std::vector<int> processData(bool flag) {
std::vector<int> data;
if (flag) {
data = {1, 2, 3};
return data; // 路径1
} else {
data = {4, 5, 6};
return data; // 路径2
}
}
这种情况下,编译器可能无法确定哪个路径会被执行,所以NRVO可能失效。但即便如此,你也不应该手动加std::move。因为C++标准规定,在返回局部变量时,编译器会自动尝试将其视为右值。也就是说,即使NRVO没生效,编译器也会自动使用移动语义。
小技巧:如果你不确定编译器是否做了NRVO,可以加编译选项查看。GCC和Clang都支持-fno-elide-constructors来禁用拷贝省略。用这个选项编译你的代码,看看有没有额外的构造调用。
总结一下
说白了,返回局部变量时显式使用std::move,属于「好心办坏事」。编译器比你更懂怎么优化这段代码,你强行干预反而会破坏优化机会。
记住这个原则:
返回局部变量时,直接返回变量名即可。不要加std::move,不要加std::forward,什么都不要加。让编译器做它该做的事。
我见过太多人在这上面栽跟头了。你想想看,本来代码跑得好好的,你加了个std::move,性能反而下降了,这不是自找麻烦吗?
最后送大家一句话:在C++里,有时候「什么都不做」就是最好的优化。
公众号:蓝海资料掘金营,微信deep3321