C++右值引用详解

次浏览

概述

右值引用(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;
};

最佳实践

  1. 使用 std::move 转移所有权:当你不再需要一个对象时
  2. 使用 std::forward 完美转发:在泛型代码中保持值类别
  3. 移动操作标记为 noexcept:允许标准库容器优化
  4. 移动后的对象不要使用:处于"有效但未定义"状态
  5. 优先使用 std::make_unique/std::make_shared:避免不必要的拷贝

总结

特性 用途
右值引用 T&& 绑定到临时对象
移动语义 转移资源所有权,避免拷贝
std::move 将左值转换为右值引用
std::forward 保持参数的原始值类别
万能引用 模板中既能接受左值也能接受右值

右值引用是现代 C++ 性能优化的基石,理解它能够帮助你写出更高效的代码。


参考资料

使用 Hugo 构建
主题 StackJimmy 设计