oop::notes

std::move 其实什么都不移动

2026-06-30 · #cpp#move-semantics

名字起得不好是很多困惑的来源。std::move 本身不产生任何指令,它的实现基本就是一个 static_cast:

template <class T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

它做的事情只是把一个表达式变成右值(xvalue),让重载决议有机会选中移动构造函数或移动赋值运算符。真正"搬东西"的是那些函数。

所以 const 对象 move 了也白 move

const std::string s = "hello";
std::string t = std::move(s);   // 调用的是拷贝构造

std::move(s) 的类型是 const std::string&&,移动构造函数接受的是 std::string&&,绑定不上,于是退回到 const std::string& 的拷贝构造。编译器不会报错,clang-tidy 的 performance-move-const-arg 能查出来。

return 时不要写 std::move

std::vector<int> make() {
    std::vector<int> v(1000);
    return std::move(v);   // 不好
}

直接 return v; 时,编译器可以做 NRVO,连移动都省掉。加了 std::move,返回表达式不再是一个简单的局部变量名,NRVO 就失效了,只能退化成一次移动。GCC 的 -Wpessimizing-move 会对这种写法给出警告。

即使 NRVO 没发生,标准也规定返回局部变量时会先尝试把它当右值处理,所以本来就会移动,手写 std::move 没有任何好处。

被移动后的对象

标准库类型被移动后处于"有效但未指定"的状态:可以析构、可以重新赋值,但不应该假设它的内容。比如被移动后的 std::string 在大多数实现里是空的,但这不是标准保证的。

std::string a = "data";
std::string b = std::move(a);
a.clear();      // 先恢复到已知状态再用
a = "new";      // 重新赋值也可以