右值引用
右值引用 是用 T&& 声明的一种引用,专门用来绑定 即将被销毁、不会再使用的对象,从而安全地偷走它的资源。
右值引用语法
cpp
int &&rref1 {100}; // 右值引用 绑定到 字面量
int a {10}, b{20};
int &&rref2 {a + b}; // 右值引用 绑定到 表达式结果为什么要有右值引用?
cpp
// C++11 before
std::string make() {
std::string s = "hello";
return s;
}哪怕 s 即将要被销毁,语言也得老老实实 拷贝它的内部资源。
这在逻辑和性能上是荒谬的:我明明知道你不要了,却还要完整地复制一份。
右值引用的应用
移动语义
右值引用的核心用途是实现 移动构造函数 和 移动赋值运算符,将资源移动,避免不必要的拷贝。
cpp
class Buffer {
private:
char *data_;
size_t size_;
public:
Buffer(size_t size) : data_(new char[size]), size_(size) {}
// 移动构造函数
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() { delete[] data_; }
};
// 使用 std::move 可以将左值强制转换为右值
Buffer b1(100);
Buffer b2 = std::move(b1); // 调用移动构造函数,不再拷贝完美转发
结合模板和 std::forward,右值引用可以保留参数的原始值类别(左值/右值):
cpp
template <typename T>
void wrapper(T&& arg) {
real_function(std::forward<T>(arg)); // 保留参数的值类别
}std::move 的本质
std::move 并不移动任何东西,它只是一个强制类型转换:
cpp
// std::move 的实现(简化版)
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& arg) noexcept {
return static_cast<std::remove_reference_t<T>&&>(arg);
}它只是将参数强制转换为右值引用,告诉编译器"我可以被移动"。
常见误区
cpp
std::string s1 = "hello";
std::string s2 = std::move(s1); // 移动后,s1 仍然存在!
// 移动后 s1 的状态是"有效但未指定",可以安全使用,但不能依赖其值
std::cout << s1 << "\n"; // 可能输出空字符串或原值,未定义移动后的对象状态
被移动后的对象处于有效但未指定(valid but unspecified)状态:
cpp
std::string s1 = "hello";
std::string s2 = std::move(s1);
// s1 现在是"有效但未指定"状态:
// - 可以调用 s1 的成员函数
// - 可以给 s1 赋值
// - 但不能假设 s1 的值是什么
s1 = "new value"; // OK,可以重新赋值
std::cout << s1.size(); // OK,可以获取状态最佳实践:移动源对象通常应该被置为有效且可析构的状态(如设为空)。
移动语义与 noexcept
移动构造函数和移动赋值运算符应该标记 noexcept:
cpp
class Buffer {
public:
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
};为什么 noexcept 很重要?
cpp
std::vector<Buffer> vec;
// 如果移动构造函数不是 noexcept
// vector 扩容时会选择拷贝而非移动!
vec.push_back(buffer); // 可能触发拷贝而不是移动标准库容器在重新分配内存时,会优先使用移动语义,但前提是移动操作是 noexcept 的。否则会回退到拷贝。
隐式移动(C++11/14)
C++11 后,编译器在某些场景下会自动生成移动操作:
何时自动生成移动构造函数?
当以下条件满足时,编译器会生成默认移动构造函数:
- 没有显式声明拷贝构造函数
- 没有显式声明移动赋值运算符
- 没有显式声明析构函数
cpp
class Widget {
std::string name_;
std::vector<int> data_;
// 编译器会自动生成移动构造函数
};
Widget w1;
Widget w2 = std::move(w1); // 调用自动生成的移动构造函数五法则与零法则
- 五法则:如果显式声明了拷贝构造、拷贝赋值、移动构造、移动赋值、析构中的任意一个,应该声明或禁止所有五个。
- 零法则:如果类不需要资源管理,最好一个都不声明,让编译器生成。
cpp
// 零法则示例
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
private:
int x_, y_;
}; // 编译器会生成所有移动/拷贝函数函数重载中的右值引用
利用右值引用可以实现重载,区分左值和右值:
cpp
void process(int& x) {
std::cout << "lvalue overload\n";
}
void process(int&& x) {
std::cout << "rvalue overload\n";
}
int a{10};
process(a); // 调用 process(int&)
process(20); // 调用 process(int&&)实际应用:按值传递参数
cpp
class Widget {
public:
// 按值传递,让调用者决定传入左值还是右值
void setName(std::string name) {
name_ = std::move(name); // 移动,节省拷贝
}
private:
std::string name_;
};
std::string s = "Alice";
widget.setName(s); // 拷贝进函数,然后移动
widget.setName("Bob"); // 临时对象,直接移动左值引用和右值引用的区别?
- 左值引用
T&表达:这是一个仍有人负责的对象,不要随便动它的内部状态。 - 右值引用
T&&表达:这是一个临时对象,可以安全地进行破坏,只需要你进行善后。
移动迭代器
std::make_move_iterator 可以将普通迭代器转换为移动迭代器:
cpp
std::vector<std::string> src = {"a", "b", "c"};
std::vector<std::string> dst;
dst.reserve(src.size());
dst.assign(std::make_move_iterator(src.begin()),
std::make_move_iterator(src.end()));
// src 中的元素已被"移动",src 变为空这在需要批量移动元素时很有用。