跳到主要内容

C++ 右值引用与移动语义

右值引用和移动语义是 C++11 引入的核心特性,它们从根本上改变了 C++ 资源管理的方式,带来了显著的性能提升。理解这些概念对于编写高效的现代 C++ 代码至关重要。

值类别

左值与右值

在理解右值引用之前,需要先明确值类别的概念:

左值(lvalue) 是有名字、有地址的表达式,可以出现在赋值号的左边。左值代表一个持久的对象,在表达式结束后仍然存在。

右值(rvalue) 是没有名字、没有持久地址的表达式,只能出现在赋值号的右边。右值代表临时对象,在表达式结束后就会被销毁。

int x = 10;        // x 是左值,10 是右值
int y = x + 5; // y 是左值,x + 5 是右值
int& ref = x; // 正确:左值引用绑定到左值
// int& ref2 = 10; // 错误:左值引用不能绑定到右值

const int& cref = 10; // 正确:const 左值引用可以绑定到右值

判断左值和右值

一个简单的判断方法:如果能取地址,就是左值;否则是右值。

int x = 10;
&x; // 正确,x 是左值
// &10; // 错误,10 是右值
// &(x + 1); // 错误,x + 1 是右值

std::string s1 = "hello";
std::string s2 = "world";
std::string s3 = s1 + s2; // s1 + s2 是右值(临时对象)

&s1; // 正确,s1 是左值
// &(s1 + s2); // 错误,s1 + s2 是右值

常见例子

// 左值示例
int x = 10; // x 是左值
x = 20; // 可以赋值
int arr[5] = {1,2,3,4,5};
arr[0]; // 下标表达式是左值
*x; // 解引用是左值
std::string s;
s; // 变量是左值

// 右值示例
10; // 字面量是右值
x + 1; // 算术表达式结果是右值
x++; // 后置自增返回右值
std::string("hello"); // 临时对象是右值
s1 + s2; // 字符串拼接结果是右值
foo(); // 函数返回非引用类型是右值

右值引用

基本语法

右值引用使用 && 声明,只能绑定到右值:

int x = 10;

// 左值引用:只能绑定到左值
int& lr = x; // 正确
// int& lr2 = 10; // 错误

// 右值引用:只能绑定到右值
int&& rr = 10; // 正确
// int&& rr2 = x; // 错误

// const 左值引用:可以绑定到任何值
const int& clr = x; // 正确
const int& clr2 = 10; // 正确

右值引用的意义

右值引用的核心价值在于:可以修改右值,实现资源转移(移动语义)。

std::string s1 = "hello";
std::string s2 = "world";

// 传统方式:s1 + s2 创建临时对象,然后拷贝给 s3,临时对象销毁
std::string s3 = s1 + s2; // 可能涉及内存分配和拷贝

// 使用右值引用:直接"窃取"临时对象的资源
std::string&& rref = s1 + s2; // 绑定到临时对象
// 可以修改临时对象的内容
rref += "!"; // 直接修改临时对象

std::move

std::move 是将左值转换为右值引用的工具:

#include <utility>  // std::move

std::string str = "hello";

// str 是左值
std::string& ref = str;

// std::move(str) 返回右值引用
std::string&& rref = std::move(str);

// 注意:std::move 只是类型转换,不执行任何移动
// 移动操作发生在赋值或构造时

std::string str2 = std::move(str); // 移动构造
// 此时 str 处于"移后状态"(可能为空,但不保证)

理解 std::move:

std::move 本质上是一个类型转换函数:

// std::move 的简化实现
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}

// 它只是将参数转换为右值引用,真正的移动由移动构造函数完成

移动语义

问题:不必要的拷贝

考虑一个持有动态内存的类:

#include <cstring>

class MyString {
private:
char* data;
size_t length;

public:
// 构造函数
MyString(const char* s = "") {
length = strlen(s);
data = new char[length + 1];
strcpy(data, s);
std::cout << "构造: " << data << std::endl;
}

// 拷贝构造函数
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝构造: " << data << std::endl;
}

// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝赋值: " << data << std::endl;
}
return *this;
}

// 析构函数
~MyString() {
std::cout << "析构: " << (data ? data : "null") << std::endl;
delete[] data;
}

const char* c_str() const { return data; }
};

// 问题示例
MyString createString() {
MyString s("hello");
return s; // 返回时发生拷贝?
}

void problemDemo() {
MyString s1 = createString(); // 多次拷贝!
MyString s2 = s1; // 拷贝构造
s2 = s1; // 拷贝赋值
}

解决:移动构造和移动赋值

class MyString {
private:
char* data;
size_t length;

public:
// 构造函数
MyString(const char* s = "") {
length = strlen(s);
data = new char[length + 1];
strcpy(data, s);
}

// 拷贝构造函数
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝构造" << std::endl;
}

// 移动构造函数
MyString(MyString&& other) noexcept {
// 直接"窃取"资源
data = other.data;
length = other.length;

// 将源对象置于安全状态
other.data = nullptr;
other.length = 0;

std::cout << "移动构造" << std::endl;
}

// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "拷贝赋值" << std::endl;
}
return *this;
}

// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
// 释放自己的资源
delete[] data;

// 窃取对方的资源
data = other.data;
length = other.length;

// 将对方置于安全状态
other.data = nullptr;
other.length = 0;

std::cout << "移动赋值" << std::endl;
}
return *this;
}

~MyString() {
delete[] data;
}
};

// 使用示例
void moveDemo() {
MyString s1("hello");

// 移动构造
MyString s2 = std::move(s1); // s1 的资源转移给 s2

// 移动赋值
MyString s3;
s3 = std::move(s2); // s2 的资源转移给 s3

// 返回值优化
MyString s4 = createString(); // 可能直接移动或 RVO
}

移动语义的本质

移动语义的核心思想是:不进行深拷贝,而是转移资源的所有权。

拷贝语义:创建新资源,复制数据(慢)
┌───────────┐ ┌───────────┐
│ 原对象 │ ──────> │ 新对象 │
│ data ────────┐ │ data ────────┐
│ length │ │ │ length │ │
└───────────┘ │ └───────────┘ │
▼ ▼
[内存块] [内存块(副本)]

移动语义:转移资源指针(快)
┌───────────┐ ┌───────────┐
│ 原对象 │ │ 新对象 │
│ data=null│ │ data ────────┐
│ length=0 │ │ length │ │
└───────────┘ └───────────┘ │

[内存块]

std::forward 和完美转发

引用折叠

当模板参数推导和引用结合时,会发生引用折叠:

template<typename T>
void func(T&& param) {}

int x = 10;

// 传入左值:T 推导为 int&,T&& 折叠为 int&
func(x); // T = int&, T&& = int& && = int&

// 传入右值:T 推导为 int,T&& 保持为 int&&
func(10); // T = int, T&& = int&&

// 引用折叠规则:
// & & = &
// & && = &
// && & = &
// && && = &&

完美转发

完美转发是指在函数模板中将参数以原始的值类别(左值或右值)转发给其他函数:

#include <utility>

void process(int& x) {
std::cout << "左值引用" << std::endl;
}

void process(int&& x) {
std::cout << "右值引用" << std::endl;
}

// 错误的转发:参数总是左值
template<typename T>
void wrongForward(T&& param) {
process(param); // param 是左值!总是调用左值版本
}

// 完美转发:保持原始值类别
template<typename T>
void perfectForward(T&& param) {
process(std::forward<T>(param)); // 正确转发
}

int main() {
int x = 10;

wrongForward(x); // 输出:左值引用
wrongForward(10); // 输出:左值引用(错误!应该是右值)

perfectForward(x); // 输出:左值引用
perfectForward(10); // 输出:右值引用(正确!)

return 0;
}

std::forward 的实现

// std::forward 的简化实现
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}

template<typename T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value,
"不能将右值转发为左值");
return static_cast<T&&>(t);
}

完美转发的应用

#include <utility>
#include <memory>

// 工厂函数:完美转发参数给构造函数
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// 包装函数:保持参数的值类别
template<typename Func, typename... Args>
auto invoke(Func&& f, Args&&... args)
-> decltype(std::forward<Func>(f)(std::forward<Args>(args)...)) {
return std::forward<Func>(f)(std::forward<Args>(args)...);
}

规则和最佳实践

五法则(Rule of Five)

如果定义了以下任何一个,就应该定义全部五个:

  1. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符
  4. 移动构造函数
  5. 移动赋值运算符
class Resource {
private:
int* data;
size_t size;

public:
// 1. 构造函数
Resource(size_t n = 0) : size(n), data(n ? new int[n] : nullptr) {}

// 2. 析构函数
~Resource() {
delete[] data;
}

// 3. 拷贝构造函数
Resource(const Resource& other)
: size(other.size), data(other.size ? new int[other.size] : nullptr) {
std::copy(other.data, other.data + size, data);
}

// 4. 移动构造函数
Resource(Resource&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}

// 5. 拷贝赋值运算符
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = size ? new int[size] : nullptr;
std::copy(other.data, other.data + size, data);
}
return *this;
}

// 6. 移动赋值运算符
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};

零法则(Rule of Zero)

最佳实践:让编译器自动生成所有特殊成员函数:

#include <vector>
#include <string>

// 使用标准库容器,不需要手动定义任何特殊成员函数
class Person {
std::string name;
std::vector<int> scores;

public:
Person(const std::string& n) : name(n) {}

// 编译器自动生成:
// - 析构函数
// - 拷贝构造函数
// - 移动构造函数
// - 拷贝赋值运算符
// - 移动赋值运算符
// 都会正确工作!
};

移动语义的注意事项

// 1. 移动后的对象处于"有效但未指定"状态
std::string s1 = "hello";
std::string s2 = std::move(s1);
// s1 现在处于有效但未指定状态
// 可以安全地赋值、销毁,但不要假设其内容
s1 = "world"; // 正确

// 2. 不要移动后会自动复用的对象
void badExample() {
std::string str = "hello";
process(std::move(str)); // 移动 str
std::cout << str << std::endl; // 危险!str 可能是空的
}

// 3. 移动构造函数应该标记为 noexcept
class MyClass {
public:
MyClass(MyClass&& other) noexcept { // 重要!
// ...
}
};
// 这允许标准库容器在重分配时使用移动而非拷贝

// 4. const 对象不能被移动
const std::string cs = "hello";
std::string s = std::move(cs); // 实际上是拷贝!
// 因为 const 右值引用会退化为 const 左值引用

// 5. 成员变量的移动
class Container {
std::vector<int> data;
std::string name;

public:
Container(Container&& other) noexcept
: data(std::move(other.data)), // 移动成员
name(std::move(other.name)) {} // 移动成员
};

实际应用

高效的 swap 实现

// 通用的 swap 使用移动语义
template<typename T>
void mySwap(T& a, T& b) {
T temp = std::move(a); // 移动构造
a = std::move(b); // 移动赋值
b = std::move(temp); // 移动赋值
}

// std::swap 已经使用这种方式实现

高效的容器操作

#include <vector>
#include <algorithm>

void containerDemo() {
std::vector<std::string> vec;

// 移动插入而非拷贝插入
std::string s = "hello";
vec.push_back(std::move(s)); // s 的内容移动到 vector

// 原地构造(C++11)
vec.emplace_back("world"); // 直接在 vector 中构造

// 移动整个 vector
std::vector<std::string> vec2 = std::move(vec);
// vec 现在为空
}

工厂函数

template<typename T, typename... Args>
std::unique_ptr<T> create(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}

// 使用
auto person = create<Person>("张三", 25);

小结

本章学习了:

  1. 值类别:左值和右值的区别
  2. 右值引用T&&std::move
  3. 移动语义:移动构造函数和移动赋值运算符
  4. 完美转发std::forward 和引用折叠
  5. 五法则和零法则:何时需要定义特殊成员函数
  6. 最佳实践:正确使用移动语义的规则

下一步

接下来让我们学习 C++ Lambda 表达式,了解函数式编程特性。