Bootstrap

C++右值引用简介

一、前言

C++ 的右值引用(rvalue reference)是在 C++11 中引入的,它主要用于实现移动语义和完美转发,极大地提高了程序的性能和灵活性。

二、左值和右值

左值(Lvalue): 可以在程序中取地址的值,通常表示持久性的对象。例如,变量、数组元素、对象成员等。
右值(Rvalue): 不能取地址的临时值,通常是表达式的结果。例如,3, x + y等。
可以形象记忆为可以取地址,位于等号左边就是左值不可以取地址,位于等号右边就是右值

三、左值引用和右值引用

左值引用: 标志是 ‘&’ ,能指向左值不能指向右值的就是左值引用。(存在特殊的情况就是const左值引用可以指向右值:const int &a = 5。)
右值引用: 标志是 ‘&&’,可以指向右值不能指向左值的就是右值引用。(也存在特殊情况可以使用std::move()把左值变为右值来供右值引用使用:int &&a = std::move(b)。)

四、右值引用的主要用途

1、移动语义(Move Semantics)

移动语义允许对象的资源从一个对象“移动”到另一个对象,而不是复制它们。通过 std::move(),可以将对象显式转换为右值引用,从而调用移动构造函数或移动赋值运算符。

class MyClass {
public:
    MyClass() { std::cout << "Default Constructor\n"; }
    MyClass(const MyClass&) { std::cout << "Copy Constructor\n"; }
    MyClass(MyClass&&) { std::cout << "Move Constructor\n"; }
};

int main() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1);  // 调用移动构造函数
    return 0;
}
// 输出如下
// Default Constructor
// Move Constructor

在这个例子中,std::move(obj1) 将 obj1 转换为右值引用,从而调用 MyClass 的移动构造函数,而不是复制构造函数。这可以避免深拷贝的开销,提高性能。

2、完美转发(Perfect Forwarding)

在泛型编程中,有时我们希望将参数原封不动地传递给另一个函数,而不管它是左值还是右值。这时可以使用 std::forward 与右值引用配合,实现完美转发。

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

void process(int& x) { std::cout << "Lvalue reference\n"; }
void process(int&& x) { std::cout << "Rvalue reference\n"; }

int main() {
    int a = 10;
    wrapper(a);        // 输出: Lvalue reference
    wrapper(20);       // 输出: Rvalue reference
    return 0;
}

(1)int& x表示左值引用,接受可以取地址的变量。
int&& x表示右值引用,接受不能取地址的临时对象或通过std::move转换后的对象。
(2)在模板上下文中,T&&不是单纯的右值引用,而是一种“万能引用”(Universal Reference)。当传递给wrapper函数的参数是左值时,T推导为int&,此时T&&会折叠为int& &,也就是int&。当参数是右值时,T推导为int,此时T&&就是int&&。
(3)std::forward(arg)根据T的推导情况,决定是否转发为左值或右值。如果T是左值引用类型(如int&),std::forward将保持其为左值;如果T是非引用类型(如int),std::forward将其转发为右值。

  • 完美转发的应用场景
// 通用工厂函数:在工厂函数中,创建对象时保留传入参数的左值/右值属性。
template<typename T, typename... Args>
std::shared_ptr<T> make_shared_object(Args&&... args) {
    return std::make_shared<T>(std::forward<Args>(args)...);
}

// 包装函数:编写包装函数时,转发所有参数给另一个函数,确保原有的左值/右值属性不变。
template<typename Func, typename... Args>
auto invoke(Func&& func, Args&&... args) {
    return std::forward<Func>(func)(std::forward<Args>(args)...);
}

// 高效转移资源:在需要高效转移资源的场景中,完美转发可以避免不必要的拷贝,提升性能。

五、移动构造函数与移动赋值运算符

为了充分利用右值引用和移动语义,C++11 引入了移动构造函数和移动赋值运算符。
移动构造函数: 接受一个右值引用参数,并且通常将资源从传入对象“移动”到新对象中。它的主要目的是避免不必要的深拷贝,减少资源开销。

class MyClass {
private:
    int* data;

public:
    // 默认构造函数
    MyClass(int size) : data(new int[size]) {
        std::cout << "Constructing MyClass\n";
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
        std::cout << "Moving MyClass\n";
        other.data = nullptr; // 将其他对象的指针置为nullptr,防止双重释放
    }

    // 析构函数
    ~MyClass() {
        std::cout << "Destroying MyClass\n";
        delete[] data;
    }

    // 禁用拷贝构造函数(可选)
    MyClass(const MyClass&) = delete;

    // 禁用拷贝赋值运算符(可选)
    MyClass& operator=(const MyClass&) = delete;
};

int main() {
    MyClass obj1(10);            // 调用默认构造函数
    MyClass obj2(std::move(obj1)); // 调用移动构造函数
    return 0;
}
// 输出如下
// Constructing MyClass
// Moving MyClass
// Destroying MyClass

移动构造函数的实现: MyClass(MyClass&& other) noexcept : data(other.data)中,other是一个右值引用,指向要被移动的对象。我们将other.data的资源直接移动给当前对象,并将other.data置为nullptr,以防止other在其析构时再次释放该资源。

std::move 的作用: std::move(obj1)将obj1转化为右值引用,从而使其可以匹配移动构造函数。std::move本质上并不移动对象,而是将其类型转换为右值引用类型。

noexcept 关键字: 在移动构造函数中,noexcept关键字表明此函数不会抛出异常。这是一个很好的实践,因为某些标准库容器(如std::vector)要求其元素的移动操作是无异常的,以进行某些优化。

移动构造函数被调用的情况:

// 返回局部对象
MyClass createMyClass() {
    MyClass temp(10);
    return temp; // 移动构造函数可能在这里调用
}

// 传递临时对象
MyClass obj = MyClass(10); // 临时对象会通过移动构造函数传递给 obj

// 通过std::move显式地将对象转为右值引用
MyClass obj1(10);
MyClass obj2(std::move(obj1)); // 使用 std::move 转为右值引用

移动赋值运算符: 类似于移动构造函数,但它是用于对象已经存在时的赋值。

class MyClass {
private:
    int* data;

public:
    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        std::cout << "Move assigning MyClass\n";
        if (this != &other) {
            delete[] data;            // 释放当前对象的资源
            data = other.data;        // 接管其他对象的资源
            other.data = nullptr;     // 将其他对象的指针置为 nullptr
        }
        return *this;
    }
    // 其他构造函数和析构函数省略...
};

int main() {
    MyClass obj1(10);
    MyClass obj2(20);
    obj2 = std::move(obj1); // 调用移动赋值运算符
}
// 输出如下
// Move assigning MyClass
// Destroying MyClass

六、总结

右值引用是 C++11 中引入的强大特性,主要用于实现移动语义和完美转发。通过右值引用,C++ 可以避免不必要的对象复制,提高程序的性能,特别是在处理大量数据或复杂对象时。掌握右值引用和相关的移动语义对现代 C++ 编程至关重要。

;