std::move 与 std::forward 以及左右值
值类别
- lvalue: 左值,可取地址,如变量名。
- prvalue: 纯右值,如
int(42)临时量。 - xvalue: 将亡值, 如表达式
std::move(obj)。
纯右值和将亡值都统称右值(rvalue),可以是不具名的临时变量,可以是即将离开作用域或被 move 的类型。左值和将亡值都属于泛左值。
Attention
值类别(value category)只看“表达式的静态属性”,跟对象本身的状态无关。变量本身没有值类别,只有表达式才有。
1
2int x = 42;
decltype(auto) b = std::move(x);b 这个标识符时,它所在的表达式是左值(因为具名变量都是左值表达式),但它的声明类型仍然是 int&&。
std::move()
Attention
std::move 只是“告诉编译器这个表达式可以当右值用”;对象是否、何时、如何被掏空,取决于随后真正被调用的移动操作。
std::move 无条件把表达式强制转换为右值引用类型的 xvalue 表达式(类型是 int&&,类别是 xvalue 将亡值)。
- move 后,原对象仍有效,不一定为空。
- 只能对可移动类型使用(有移动构造函数),否则 move 退化为拷贝。
const对象永远只能走拷贝。 std::move本身什么也不做。不移动对象,也不置空对象;具体操作取决于随后调用的移动构造/赋值函数。std::move实现了移动语义,为了避免不必要的深拷贝,如std::vector/string,来提高程序性能。
Example
1
2
3std::string a = "hello";
std::string b = std::move(a);
// a 现在处于未指定状态,通常 a.size()==0,因为 string 的移动构造把内部指针设为 nullptr。std::move(a) 只是把名字 a 对应的表达式强制变成右值(rvalue);真正“掏空” a 的是随后**= 调用被匹配的 std::string 的移动构造函数**。
std::swap 的典型实现如下
1
2
3
4
5
6template<class T>
void swap(T& a, T& b) {
T tmp = std::move(a); // 把 a 当成右值,调用移动构造
a = std::move(b); // 把 b 当成右值,调用移动赋值
b = std::move(tmp); // 把 tmp 当成右值,调用移动赋值
}
永远不要在 return 语句上
std::move临时对象或局部变量,会影响 RVO/NRVO(具名)返回值优化。
悬垂引用
std::move(obj) 中的“临时对象” obj 在整条语句结束后立即析构,指向已释放内存 → 悬垂引用。
Tip
任何右值(尤其是 std::move 后的 xvalue)不要直接绑定到引用;如果一定要延长生命周期,请用值接收,而不是引用。
std::forward()
模板函数中,模板参数 T&& 代表万能引用,实际类型是 T& 还是 T&& → 由引用折叠决定。
| 传进来的实参 | 模板形参 T 推断为 | T&& 折叠后 | 最终形参类型 |
|---|---|---|---|
| 左值 | T = U& |
U& && |
U& |
| 右值 | T = U 或 U&& |
U&& && |
U&& |
std::forward<T>(arg) 做的事情是:如果原始传入的参数是右值,它就将 arg 转换为右值;如果原始传入的参数是左值,它就什么都不做,让 arg 保持为左值。它完美地“转发”了参数的值类别。
完美转发范式
1
2
3
4template <class F, class... Args>
decltype(auto) invoke(F&& f, Args&&... args) {
return std::forward<F>(f)(std::forward<Args>(args)...);
}
Args&&...是万能引用包。std::forward把每个参数原样转发给目标函数。
1
2
3
4template<typename Func, typename... Args>
decltype(auto) wrapper(Func&& f, Args&&... args) {
return std::forward<Func>(f)(std::forward<Args>(args)...);
}
- 配合
decltype(auto)可推导模板函数返回类型,实现返回值的完美转发。
1
2
3auto generic_lambda = [](auto&&... args) {
return some_function(std::forward<decltype(args)>(args)...);
};
- 配合
auto&&可用于完美转发泛型 lambda 函数。
push_back/emplace的形参是万能引用。push_back()左值时调用拷贝构造一次深拷贝,右值时调用移动构造。对于可移动但不可拷贝的类型(如std::unique_ptr),push_back(std::move(ptr))可以;push_back(ptr)(左值)则编译不过。
总结
std::move 是“标记为可移动”,std::forward 是“原样转发”,两者都不移动对象,真正移动的是构造函数/赋值运算符。
- 当函数内部「不再用」该值,且知道一定是右值 → 直接
std::move。 - 当函数要把参数「继续往下传」,要保持其原来的值类别 →
std::forward。
| 场景 | 实际调用哪一个构造/赋值? |
|---|---|
T a = b; |
拷贝构造(T(const T&)) |
T a = std::move(b); |
移动构造(T(T&&)) |
a = b; |
拷贝赋值(T& operator=(const T&)) |
a = std::move(b); |
移动赋值(T& operator=(T&&)) |
| 目的 | 用哪个 |
|---|---|
| 在函数内部把参数/成员「搬走」 | std::move |
| 把参数继续转发给下一层函数 | std::forward |
| 实现 push_back/emplace 的完美转发 | std::forward |
| 实现 swap | std::move |
| 实现 move ctor / move assign | std::move |
其他
POD
“平凡可复制 POD” = 同时满足 ①平凡类型(trivial)②可复制(trivially copyable)③POD(standard-layout) 的类型;在 C++20 之前这三者等价,C++20 起 POD 被拆分成 “trivial + standard-layout”, 日常口语仍把 int、double 这类简单数据叫 POD。
能直接 memcpy、没有析构/构造负担、布局与 C 结构体一致的类型,就是“平凡可复制 POD”。push/emplace 时传入的参数类型若是 POD ,则都变为同一条汇编写入容器内存,无性能差别。
规则
| 条件 | 含义 | 典型检查 |
|---|---|---|
平凡类型 (std::is_trivial_v) |
默认构造/析构/拷贝/移动都“什么都不做” | 没有用户提供的构造/析构 |
平凡可复制 (std::is_trivially_copyable_v) |
可用 memcpy/memmove 复制 |
同上,再加没有虚函数、引用成员 |
标准布局 (std::is_standard_layout_v) |
内存布局与 C 结构体兼容 | 没有多继承、虚函数、不同访问权限混合 |
常见平凡可复制 POD 类型
| 类别 | 例子 |
|---|---|
| 算术类型 | bool, char, short, int, long, float, double |
| 枚举 | enum Color { Red, Green, Blue }; |
| 指针 | int*, void*, MyClass* |
| 简单结构体 | struct P { int x; double y; }; |
| C 兼容数组 | int[4], double[2][3] |
| 平凡聚合 | struct Vec3 { float x, y, z; }; |
常见非平凡可复制 POD 的例子
- std::string(有非平凡析构)
- std::vector
(动态内存管理) - 含虚函数的类(虚表指针)
- 含引用成员的类(引用不可 memcpy)
- 含 std::unique_ptr 的类(独占所有权)
引用折叠
C++ 语法不允许直接声明“引用的引用”,比如 int& & x; 是非法的。但在模板实例化过程中,这种结构会间接出现。引用折叠规则定义了如何将这种“引用的引用”简化为单个引用:
A& &->A&(左值引用的左值引用 -> 左值引用)A& &&->A&(左值引用的右值引用 -> 左值引用)A&& &->A&(右值引用的左值引用 -> 左值引用)A&& &&->A&&(右值引用的右值引用 -> 右值引用)
核心规则:只要有&(左值引用)出现,最终结果就是&。只有当所有引用都是&&(右值引用)时,结果才是&&。
因此模板函数在 T&& 下可以实现万能引用。
RVO/NRVO
1 | std::string make() { |
- 未开启 RVO 时(两次构造 + 一次析构):
① 在 make 里构造临时std::string→ ② 用临时对象拷贝/移动到s→ ③ 销毁临时对象。
开启 RVO 后:
临时对象直接在 s 的内存上构造,只构造一次。 - 未开启 NRVO 时(两次构造 + 一次析构):
① 在 make 里构造临时result→ ②result拷贝/移动到s→ ③ 销毁result。
开启 RVO 后:result直接在 s 的内存上构造,只构造一次,函数里对result的所有操作实际都在这块内存上进行。

