概述
右值引用(Rvalue Reference)是 C++11 引入的重要特性,它是实现移动语义和完美转发的基础。理解右值引用对于编写高效的 C++ 代码至关重要。
左值与右值
什么是左值?
左值(lvalue)是指有名字、有地址的表达式,可以取地址,可以出现在赋值号的左边。
1
2
3
4
5
6
|
int x = 10; // x 是左值
int arr[5]; // arr 是左值
std::string s; // s 是左值
x = 20; // 正确:左值可以赋值
int* p = &x; // 正确:左值可以取地址
|
什么是右值?
右值(rvalue)是指没有名字、临时的表达式,通常是字面量或表达式的求值结果,不能取地址。
1
2
3
4
5
6
|
int x = 10; // 10 是右值(字面量)
int y = x + 5; // x + 5 的结果是右值(临时值)
std::string s = std::string("hello"); // std::string("hello") 是右值
int* p = &10; // 错误:右值不能取地址
10 = 20; // 错误:右值不能被赋值
|
左值引用 vs 右值引用
1
2
3
4
5
6
7
8
9
10
|
int x = 10;
int& lr = x; // 左值引用,绑定到左值
int& lr2 = 10; // 错误:左值引用不能绑定到右值
const int& clr = 10; // 正确:const左值引用可以绑定到右值
int&& rr = 10; // 右值引用,绑定到右值
int&& rr2 = x; // 错误:右值引用不能绑定到左值
int&& rr3 = std::move(x); // 正确:std::move 将左值转为右值
|
右值引用语法
右值引用使用 && 声明:
1
2
3
4
|
int&& r1 = 42; // 绑定到字面量
double&& r2 = 3.14; // 绑定到临时 double
std::string&& r3 = std::string("hi"); // 绑定到临时 string
std::string&& r4 = "hello"; // 绑定到临时 string(隐式转换)
|
移动语义
为什么需要移动语义?
考虑下面的代码:
1
2
3
4
5
6
|
std::string createString() {
std::string s = "Hello, World!";
return s; // 传统做法会拷贝整个字符串
}
std::string result = createString(); // 发生拷贝
|
在没有移动语义的情况下,函数返回时会触发拷贝构造函数,分配新的内存并复制所有字符,这是不必要的开销。
移动构造函数
移动构造函数"窃取"资源的所有权:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class MyString {
private:
char* data;
size_t len;
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data(other.data), len(other.len) {
other.data = nullptr; // 源对象置空
other.len = 0;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
len = other.len;
other.data = nullptr;
other.len = 0;
}
return *this;
}
};
|
std::move
std::move 是一个类型转换工具,将左值转换为右值引用:
1
2
3
|
std::string s1 = "hello";
std::string s2 = std::move(s1); // 移动而非拷贝
// 此时 s1 处于"有效但未定义"状态,不应再使用
|
⚠️ 注意:std::move 本身不移动任何东西,它只是一个类型转换。真正的移动发生在移动构造函数或移动赋值运算符中。
完美转发
问题场景
1
2
3
4
5
6
7
8
9
10
11
|
template<typename T>
void wrapper(T arg) {
process(arg); // arg 始终是左值,丢失了原始的值类别
}
void process(int& x) { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }
int x = 10;
wrapper(x); // 输出 "lvalue"
wrapper(10); // 还是输出 "lvalue"(预期应该是 "rvalue")
|
std::forward
std::forward 配合万能引用实现完美转发:
1
2
3
4
5
6
7
8
|
template<typename T>
void wrapper(T&& arg) { // 万能引用
process(std::forward<T>(arg)); // 完美转发
}
int x = 10;
wrapper(x); // 输出 "lvalue"
wrapper(10); // 输出 "rvalue"
|
万能引用(Universal Reference)
当 T&& 出现在模板参数推导中时,它是万能引用:
1
2
3
4
5
6
|
template<typename T>
void func(T&& arg); // 万能引用
int x = 10;
func(x); // T 推导为 int&,arg 的类型是 int& && → int&
func(10); // T 推导为 int,arg 的类型是 int&&
|
引用折叠规则
C++ 定义了引用折叠规则:
| 左值引用 |
右值引用 |
结果 |
| T& & |
T& && |
T& |
| T& & |
T&& & |
T& |
| T&& & |
T& && |
T& |
| T&& && |
T&& && |
T&& |
简化规则:只要有一个是左值引用,结果就是左值引用;只有两个都是右值引用,结果才是右值引用。
实际应用示例
示例1:高效的字符串连接
1
2
3
4
5
6
|
std::string concatenate(std::string a, std::string b) {
return a + b; // 移动语义避免了不必要的拷贝
}
auto result = concatenate(std::string("Hello, "),
std::string("World!"));
|
示例2:工厂函数
1
2
3
4
5
6
|
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
auto p = make_unique<std::string>("Hello");
|
示例3:资源管理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t n) : data(new int[n]), size(n) {}
~Buffer() { delete[] data; }
// 移动构造
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
// 禁用拷贝(因为资源独占)
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
};
|
最佳实践
- 使用
std::move 转移所有权:当你不再需要一个对象时
- 使用
std::forward 完美转发:在泛型代码中保持值类别
- 移动操作标记为
noexcept:允许标准库容器优化
- 移动后的对象不要使用:处于"有效但未定义"状态
- 优先使用
std::make_unique/std::make_shared:避免不必要的拷贝
总结
| 特性 |
用途 |
右值引用 T&& |
绑定到临时对象 |
| 移动语义 |
转移资源所有权,避免拷贝 |
std::move |
将左值转换为右值引用 |
std::forward |
保持参数的原始值类别 |
| 万能引用 |
模板中既能接受左值也能接受右值 |
右值引用是现代 C++ 性能优化的基石,理解它能够帮助你写出更高效的代码。
参考资料