参考
c++11 引入rvalue reference, 此特性允许程序员高效地移动资源而非拷贝资源, 可显著提高程序性能
值类别(value categories)
c++11 对表达式的值类别进行更细致的划分
lvalue
表示一个有名字、有明确内存地址(可寻址)对象, 其生命周期通常超出当前表达式
// x是一个左值
int x = 10;
rvalue
通常表示一个临时、无名、不可寻址值, 其生命周期仅存在于当前表达式中
// 5和3是右值, 它们和8也是一个右值
int y = 5 + 3;
细化分类
c++11 为支持移动语义, 将值类别进一步细化为三种基本类别: 左值(lvalue)、纯右值 (prvalue) 和 将亡值(xvalue)
- 纯右值 (
prvaluepure rvalue): 传统的右值
如字面量 10、返回非引用类型的函数调用 get_value()、临时对象 MyClass()
- 将亡值 (
xvalue):c++11新增
它有身份(可取地址), 但其资源即将被”剥夺”或”转移”, 例如 std::move(obj) 的返回值
-
泛左值 (
glvalue): 左值和将亡值的统称(有身份的表达式) -
右值 (
rvalue): 纯右值和将亡值的统称(可被安全移动的表达式)
graph TD
subgraph 表达式
direction TB
glvalue[泛左值 glvalue<br>有身份/可取地址]
rvalue[右值 rvalue<br>无身份/可移动]
end
subgraph 基本值类别
lvalue[左值 lvalue<br>有身份, 不可移动]
xvalue[将亡值 xvalue<br>有身份, 可移动]
prvalue[纯右值 prvalue<br>无身份, 不可移动]
end
glvalue --> lvalue
glvalue --> xvalue
rvalue --> xvalue
rvalue --> prvalue
style lvalue fill:#d4edda,stroke:#28a745
style xvalue fill:#fff3cd,stroke:#ffc107
style prvalue fill:#f8d7da,stroke:#dc3545
左右值判断
- 能否使用 & 取地址?
能取地址的是泛左值(左值或将亡值), 不能取地址的是纯右值
- 能否被移动?
将亡值和纯右值可以被移动, 左值不能被直接移动
int a = 10; // a 是左值 (lvalue)
int b = a + 5; // a+5 是纯右值 (prvalue)
int&& c = std::move(a); // std::move(a) 是将亡值 (xvalue)
引用
左值引用(lvalue reference)
c++98 引入的引用, 使用 & 表示, 它只能绑定到左值
int num = 10;
int& ref1 = num; // 正确: 绑定到左值
int& ref2 = 10; // 错误: 不能将纯右值绑定到非常量左值引用
常量左值引用(const lvalue reference)是一个特例, 它可以绑定到右值
这在c++98 中常用于避免参数传递时的拷贝
const int& ref3 = 10; // 正确: 常量左值引用可绑定右值
const int& ref4 = num + 5; // 正确
右值引用(rvalue reference)
c++11 新标准引入右值引用, 用 && 表示, 右值引用使得可以转移资源, 而不是复制, 避免不必要的拷贝, 从而实现移动语义
int&& rref1 = 10; // 正确: 绑定纯右值
int&& rref2 = std::move(num); // 正确: 绑定将亡值
int&& rref3 = num; // 错误: 不能直接绑定左值
右值引用必须在初始化时绑定到一个右值, 且一旦绑定, 右值引用就指向该右值
int num = 10;
// 错误: 不能将左值绑定到右值引用
int &&a = num; // 编译错误
// 正确: 右值引用绑定到右值
int &&b = 10;
移动语义(move semantics)
移动语义通过右值引用实现, 使得对象可以移动而不是拷贝, 从而提升性能
移动构造函数和移动赋值运算符是移动语义主要实现方式
移动构造函数
移动构造函数允许对象的资源从一个临时对象(右值)转移到新的对象, 而不是复制
class MyClass {
public:
int* m_data;
// 构造函数: 为 m_data 分配内存
MyClass(int value) : m_data(new int(value)) {}
// 移动构造函数: 从右值引用移动资源
MyClass(MyClass&& other) : m_data(other.m_data) {
other.m_data = nullptr; // 将 other 的资源置为空
}
// 析构函数: 释放资源
~MyClass() {
delete m_data;
}
};
移动构造函数MyClass(MyClass&& other)接收一个右值引用other, 并将其资源(m_data)转移到当前对象, 然后, 将other.mData置为空指针, 避免在析构时释放资源
移动赋值运算符
移动赋值运算符用于将一个右值的资源移动到当前对象, 避免不必要的内存分配和释放
MyClass& operator=(MyClass&& other) {
if (this != &other) {
delete m_data;
m_data = other.m_data;
other.m_data = nullptr;
}
return *this;
}
通过移动赋值运算符, 避免不必要的资源拷贝, 从而提高性能
std::move本质
std::move是一个标准库函数, 它接受一个左值并将其”转换”为右值引用, 从而可以将左值对象资源移动到另一个对象中
std::move本质上并不真正”移动”对象, 它只是将左值转换为右值引用, 使得移动语义可以生效
MyString s1("Hello");
// s1 是左值, 直接赋值会调用"拷贝构造函数"
MyString s2 = s1;
// std::move(s1) 将 s1 转换为右值引用, 触发"移动构造函数"
MyString s3 = std::move(s1);
// 此时 s1 内部的 m_data 已被置空, 处于"有效但未定义"的状态
// 不能再使用 s1 的内容, 但可以给它重新赋值或让其安全析构
sequenceDiagram
participant Main as Main 函数
participant S1 as 对象 s1 (左值)
participant Move as std::move
participant S3 as 对象 s3
Main->>S1: 创建 s1 ("Hello")
Main->>Move: std::move(s1)
Move-->>Main: 返回 static_cast<MyString&&>(s1) (将亡值)
Main->>S3: 调用移动构造函数 MyString(MyString&&)
S3->>S1: 窃取 m_data 指针
S1-->>S3: m_data 置为 nullptr
Note over S1: s1 资源被掏空
Note over S3: s3 获得 "Hello" 资源
完美转发(perfect forwarding)
右值引用的另一个核心应用是完美转发
在编写泛型包装函数(如工厂函数、线程包装器)时, 希望将参数原封不动(保持其左值/右值属性)地传递给内部函数
需要结合万能引用和 std::forward 来实现
std::forward 作用
std::forward<T>(arg) 会在 arg 是右值时将其转换为右值引用, 在 arg 是左值时保持其左值引用
#include <iostream>
#include <utility>
void process(int& x) {
std::cout << "Lvalue reference: " << x << "\n";
}
void process(int&& x) {
std::cout << "Rvalue reference: " << x << "\n";
}
// 泛型包装函数
template <typename T>
void wrapper(T&& arg) {
// 如果不使用 std::forward, arg 本身是左值, 永远只会调用 process(int&)
// 使用 std::forward 可以完美还原 arg 传入时的值类别
process(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // 传入左值, 最终调用 process(int&)
wrapper(20); // 传入右值, 最终调用 process(int&&)
wrapper(std::move(a)); // 传入将亡值, 最终调用 process(int&&)
return 0;
}
最佳实践与总结
rule of zero(零法则): 尽量使用标准库容器(如std::vector,std::string)和智能指针(std::unique_ptr,std::shared_ptr)来管理资源
这样编译器自动生成的默认构造、析构、拷贝和移动函数就能完美工作, 你不需要手动编写那五个函数
- 移动语义的适用场景:
函数返回大型局部对象时(虽然现代编译器有 RVO/NRVO 返回值优化, 但移动语义是底线保证)
向容器(如 std::vector)中插入临时对象时(使用 std::vector::push_back(std::move(obj)) 或( emplace_back)
实现工厂函数或转移对象所有权时(如 std::unique_ptr 的转移)
- 被
std::move掏空的对象
被移动后的对象处于 “有效但未定义” (Valid but unspecified state) 的状态
只能对它进行销毁(析构)或重新赋值, 绝对不能再读取它的业务数据
noexcept的重要性
自定义移动构造函数和移动赋值运算符时, 务必加上 noexcept
如果移动操作可能抛出异常, STL 容器(如 std::vector)在重新分配内存时, 为保证强异常安全(Strong Exception Guarantee), 会放弃使用移动构造, 退化为使用效率低下的拷贝构造