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::moveobj转成了右值引用,编译器反而没法做拷贝省略了——它必须调用移动构造函数。

我曾经踩过的坑:有一次在优化一个性能敏感模块时,我看到同事在返回局部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++里,有时候「什么都不做」就是最好的优化。

返回局部变量时,显式std::move的影响 不加 std::move return obj; 加了 std::move return std::move(obj); ✅ NRVO 生效 编译器直接在调用者内存构造对象 零拷贝、零移动,性能最优 ⚠️ NRVO 未生效 编译器自动使用移动语义 一次移动操作,性能可接受 ❌ 破坏NRVO 编译器被迫调用移动构造函数 比NRVO多一次移动操作 ❌ 某些场景下更糟 如果移动构造函数未定义或非noexcept 可能退化为拷贝构造,性能大幅下降 结论:返回局部变量时,直接 return obj; 即可

公众号:蓝海资料掘金营,微信deep3321