移动、转发与值类别:std::move 为什么不移动 / Move, Forward, and Value Categories
std::move这个名字很容易误导人。它本身不搬资源。它只是把一个表达式转换成“可以被当作将亡值使用”的形式,让后续构造或赋值有机会选择移动。
1. 复制为什么可能很贵
先看一个字符串:
#include <string>
std::string a = "a very long text ...";
std::string b = a;b = a 这里需要让 b 得到一份自己的内容。
大致可以理解为:
a object
|
v
buffer: "a very long text ..."
copy to b
|
v
b object
|
v
another buffer: "a very long text ..."复制的语义很好:
a 和 b 之后互不影响。但如果字符串很长、容器很大,复制可能需要分配新内存并复制大量元素。
有些时候,我们并不需要保留旧对象里的内容。
例如函数返回一个临时结果,或者把一个局部对象交给另一个对象。
这时“转移资源”比“复制资源”更合适。
2. 移动是什么
移动可以先理解成:
把对象内部拥有的资源转交给另一个对象。例子:
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string a = "hello";
std::string b = std::move(a);
std::cout << "b = " << b << '\n';
std::cout << "a = " << a << '\n';
}大致关系:
before move
a object
|
v
buffer: hello
after move construction
b object
|
v
buffer: hello
a object
|
v
valid but unspecified state注意最后一句。
移动后,a 仍然是一个有效对象。
你可以给它重新赋值:
a = "new value";但不要依赖它一定为空。
标准只保证 moved-from 对象仍然可以析构、可以赋新值、可以调用满足其前置条件的操作。
3. std::move 本身不移动
看代码:
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string s = "hello";
std::move(s);
std::cout << s << '\n';
}这段代码通常仍然打印 hello。
原因是:
std::move(s)
只产生一个表达式
这个表达式表示:s 可以被当成可移动资源来源
没有对象接收这个表达式
就没有发生移动构造或移动赋值真正移动发生在这里:
std::string t = std::move(s);因为 t 要被构造,构造函数重载可以选择移动构造。
流程:
s
|
v
std::move(s)
|
v
表达式变成可移动形态
|
v
std::string move constructor
|
v
t 接管资源所以更准确的说法是:
std::move 不是移动动作。
std::move 是允许移动的类型转换。4. 左值和右值先用身份理解
标准里的值类别很细。
入门时先抓住“有没有稳定身份”。
std::string s = "hello";
s; // 左值,有名字,有稳定身份
std::string{}; // 纯右值,临时结果
std::move(s); // 将亡值,有身份,但允许资源被复用可以先这样记:
lvalue
有身份,通常可以取地址,可以多次使用
prvalue
临时计算结果,常用来初始化对象
xvalue
有身份,但被显式标记为资源可以被转移完整分类里还有 glvalue、rvalue 等总称。
先不用急着背。
对写代码最有帮助的是:
普通有名字对象是左值。
std::move(x) 把表达式变成 xvalue。
临时对象通常可以绑定到右值引用或用于移动。5. 右值引用变量本身是左值
这句很绕,但非常重要。
看代码:
#include <string>
#include <utility>
void take(std::string&& x) {
std::string a = x;
std::string b = std::move(x);
}x 的类型是 std::string&&。
但表达式 x 有名字。
有名字的对象表达式是左值。
所以:
std::string a = x;这里更倾向于复制。
如果你确实要把 x 的资源转出去,要写:
std::string b = std::move(x);原因可以这样看:
函数参数 x
|
+-- 类型:std::string&&
|
+-- 表达式 x:lvalue,因为它有名字类型和值类别不是同一个问题。
6. 不要随便 move 还要继续读的对象
移动不是销毁。
但移动后对象内容不稳定。
危险习惯:
std::string name = "Alice";
users.push_back(std::move(name));
std::cout << name << '\n'; // 不要依赖这里的内容更合理的做法:
std::string name = "Alice";
users.push_back(std::move(name));
name = "Bob";移动后重新赋值是可以的。
但把 moved-from 对象当作仍然保存原值来读,就错了。
7. const 对象通常移动不了资源
看代码:
#include <string>
#include <utility>
const std::string s = "hello";
std::string t = std::move(s);std::move(s) 的结果大致是 const std::string&&。
移动构造通常需要修改源对象,把内部资源转走。
但 s 是 const,不能修改。
所以这里往往不能真正移动,只能复制。
这说明:
std::move 不保证移动发生。
它只影响重载选择的可能性。
真正能不能移动,还要看类型、const、可用构造函数。8. std::forward 不是更高级的 move
模板里有一种需求:
调用者传左值,我就按左值传下去。
调用者传右值,我就按右值传下去。这叫转发。
例子:
#include <utility>
void sink(const std::string&); // 接收左值或只读
void sink(std::string&&); // 接收可移动对象
template <class T>
void wrapper(T&& value) {
sink(std::forward<T>(value));
}这里的 T&& 在模板推导语境下叫转发引用。
它不是普通右值引用。
std::forward<T>(value) 会根据 T 的推导结果恢复调用者传入时的值类别。
对比:
std::move(x)
无条件把 x 转成可移动形态
std::forward<T>(x)
在模板里有条件地保留原来的左值/右值属性9. 完美转发用在哪里
典型场景是包装构造:
#include <memory>
#include <utility>
template <class T, class... Args>
std::unique_ptr<T> make_owner(Args&&... args) {
return std::unique_ptr<T>(
new T(std::forward<Args>(args)...)
);
}这和 std::make_unique 的思想类似。
调用者传什么参数,包装函数尽量不改变它们的值类别。
左值参数
保持左值,不偷偷移动
右值参数
保持右值,可以移动如果这里写成 std::move(args)...,就会把所有参数都当成可移动对象。
调用者传来的左值也可能被意外移动。
10. 本篇总结
移动语义的核心链路:
对象拥有资源
|
v
复制会创建另一份资源
|
v
移动可以转移资源
|
v
std::move 只是允许移动的表达式转换
|
v
移动构造/移动赋值才是真正执行转移的地方判断时记住:
有名字的对象表达式通常是左值。
右值引用变量本身也是左值。
std::move 后不要依赖源对象原值。
const 对象通常不能真正移动资源。
std::forward 用于模板转发,不是普通代码里的 move 替代品。下一篇讲类型推导、初始化和 constexpr。因为现代 C++ 里,值怎么传是一半,类型到底被推导成什么是另一半。