值类别

参考

  1. lvalue: 左值,可取地址,如变量名
  2. prvalue: 纯右值,如 int(42) 临时量。
  3. xvalue: 将亡值, 如表达式 std::move(obj)

纯右值和将亡值都统称右值(rvalue),可以是不具名的临时变量,可以是即将离开作用域或被 move 的类型。左值和将亡值都属于泛左值

Attention

值类别(value category)只看“表达式的静态属性”,跟对象本身的状态无关。变量本身没有值类别,只有表达式才有。

1
2
int  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
3
std::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
6
template<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 = UU&& U&& && U&&

std::forward<T>(arg) 做的事情是:如果原始传入的参数是右值,它就将 arg 转换为右值;如果原始传入的参数是左值,它就什么都不做,让 arg 保持为左值。它完美地“转发”了参数的值类别。

完美转发范式

1
2
3
4
template <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
4
template<typename Func, typename... Args>
decltype(auto) wrapper(Func&& f, Args&&... args) {
return std::forward<Func>(f)(std::forward<Args>(args)...);
}

  • 配合 decltype(auto) 可推导模板函数返回类型,实现返回值的完美转发。

1
2
3
auto 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
2
3
4
5
6
7
8
9
10
std::string make() {
return std::string("hello"); // 纯右值
}
std::string s = make(); // 无拷贝/移动!
// RVO↑ ↓NRVO
std::string make() {
std::string result = "hello"; // 局部变量
return result; // 返回具名对象
}
std::string s = make(); // 无拷贝/移动!
  • 未开启 RVO 时(两次构造 + 一次析构):
    ① 在 make 里构造临时 std::string → ② 用临时对象拷贝/移动到 s → ③ 销毁临时对象。
    开启 RVO 后:
    临时对象直接在 s 的内存上构造,只构造一次。
  • 未开启 NRVO 时(两次构造 + 一次析构):
    ① 在 make 里构造临时 result → ② result 拷贝/移动到 s → ③ 销毁 result
    开启 RVO 后:
    result 直接在 s 的内存上构造,只构造一次,函数里对 result 的所有操作实际都在这块内存上进行。