Skip to content

右值引用 ​

右值引用 是用 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 变为空

这在需要批量移动元素时很有用。