27. 右值引用

一、什么是右值引用

  右值引用是 C++11 中引入的一种重要特性,它主要用于支持移动语义和完美转发。右值引用 是对右值(即临时对象或即将被销毁的对象)的引用,允许我们直接操作这些对象的资源,而无需进行拷贝。在 C++98 中,临时对象(右值)在赋值给函数参数时,只能被接受为 const 引用,这意味着函数无法修改这些对象的值。C++11 引入右值引用后,我们可以使用非常量引用(即右值引用)来接收这些临时对象,并在函数中直接操作它们。

  左值和右值是表达式的分类,它们的主要区别在于表达式的值是否可以取地址。左值是可以取地址的表达式,而右值通常是不能取地址的表达式。

  • 左值(Lvalue):通常有明确存储地址的表达式,如变量、对象的名称等。
  • 右值(Rvalue):通常是没有明确存储地址的表达式,如字面量、临时对象、返回临时对象的表达式等。

  右值引用是对右值的引用,它使用 && 操作符来声明。右值引用的主要目的是为了实现移动语义,即允许资源的所有权从一个对象转移到另一个对象,从而避免不必要的拷贝,提高性能。

#include <iostream>

using namespace std;

int main(void)
{
    int num= 10;            // 左值

    int &a = num;           // 左值引用
    int && b = 30;          // 右值引用
    const int &c = num;     // 常量左值引用
    const int &&d = 50;     // 常量右值引用

    // 左值引用可以使用右值引用初始化
    int &e = b;
    const int &f = b;
    const int &g = d; 

    // 右值引用只能使用右值初始化
  
    return 0;
}

二、右值引用的作用

  右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,这是因为引用本身并不拥有所绑定对象的内存,它只是该对象的一个别名。通过右值引用的声明,该右值又 “重获新生”,其 生命周期与右值引用类型变量的生命周期一样,只要该变量还存活,该右值临时变量将会一直存活下去。

#include <iostream>

using namespace std;

class MyClass 
{
private:
    int *p;

public:
    MyClass(void);                          // 无参构造函数
    MyClass(const MyClass &other);          // 拷贝构造函数

    ~MyClass(void);                         // 析构函数
};

MyClass::MyClass(void) : p(new int(100))
{
    cout << "Constructor called" << endl;
}

MyClass::MyClass(const MyClass &other) : p(new int(*other.p))
{
    cout << "Copy constructor called" << endl;
}

MyClass::~MyClass(void) 
{
    cout << "Destructor called" << endl;
    delete p;
}

MyClass getMyClass(void) 
{
    MyClass temp;
    return temp;                            // 返回一个临时对象,这是一个右值
}

int main(void) 
{
    MyClass obj = getMyClass();

    return 0;
}

  这里,我们要使用命令 gcc -o template.cpp -fbi-elide-constructors 来手动编译 C++ 程序,使用命令行参数 -fno-elide-constructors 用于关闭函数返回值优化(RVO)。这是因为 GCC 的 RVO 优化会减少复制构造函数的调用。

  运行这段程序,会发现该程序发生三次构造。首先 getMyClass() 函数中 MyClass temp;会调用 无参的构造函数,然后 return temp;会使用 复制构造 产生临时对象,接着 MyClass obj = GetMyClass(); 会使用 复制构造 将临时对象复制到 obj,最后临时对象被销毁。

  但如果将 MyClass obj = getMyClass();替换为 MyClass &&obj = getMyClass(); 使用右值引用后,会调用两次构造函数,一次是 getMyClass() 中 MyClass temp 会调用 无参的构造函数,另一次是 return temp; 会使用 复制构造 产生临时对象。不同的是,由于 obj 是一个右值引用,引用的对象是函数 getMyClass() 返回的临时对象,因此该临时对象的生命周期得到延长,所以我们可以在 MyClass &&obj = getMyClass() 语句结束后继续调用该对象的其它方法而不会发生任何问题。

三、移动语义

  上述代码中 3 次构造函数的调用,不难发现第二次和第三次的复制构造是影响性能的主要原因。在这个过程中都有临时对象参与进来,而临时对象本身只是做数据的复制。如果能将临时对象的内存直接转移到 obj 对象中,就能消除内存复制对性能的消耗。在 C++11 标准中引入了 移动语义,它可以帮助我们将临时对象的内存移动到 obj 对象中,以避免内存数据的复制。

#include <iostream>

using namespace std;

class MyClass 
{
private:
    int *p;

public:
    MyClass(void);                          // 无参构造函数
    MyClass(const MyClass &other);          // 拷贝构造函数
    MyClass(MyClass &&other);               // 移动构造函数

    ~MyClass(void);                         // 析构函数
};

MyClass::MyClass(void) : p(new int(100))
{
    cout << "Constructor called" << endl;
}

MyClass::MyClass(const MyClass &other) : p(new int(*other.p))
{
    cout << "Copy constructor called" << endl;
}

MyClass::MyClass(MyClass &&other) : p(other.p) 
{
    cout << "Move constructor called" << endl;
    other.p = nullptr;
}

MyClass::~MyClass(void) 
{
    cout << "Destructor called" << endl;
    delete p;
}

MyClass getMyClass(void) 
{
    MyClass temp;
    return temp;                            // 返回一个临时对象,这是一个右值
}

int main(void) 
{
    MyClass obj1 = getMyClass();

    return 0;
}

  在上面的代码中 MyClass 类中增加了构造函数 MyClass(MyClass&& other),它的形参是一个 右值引用 类型,称为 移动构造函数

  对于 复制构造函数 而言形参是一个 左值引用,也就是说函数的实参必须是一个具名的 左值,在复制构造函数中往往进行的是 深复制,即在不能破坏实参对象的前提下复制目标对象。而 移动构造函数 恰恰相反,它接受的是一个 右值,其核心思想是通过 转移实参对象的数据 以达成构造目标对象的目的,也就是说实参对象是会被修改的。

  运行程序可以发现,后面两次的构造函数变成了 移动构造函数,因为这两次操作中源对象都是 右值(临时对象),对于右值编译器会优先选择使用移动构造函数去构造目标对象。当移动构造函数不存在的时候才会退而求其次地使用复制构造函数。在移动构造函数中使用了指针转移的方式构造目标对象,所以整个程序的运行效率得到大幅提升。

四、完美转发

  完美转发(Perfect Forwarding)是 C++11 中引入的一个特性,它允许在函数模板中将参数连同其值类别(左值或右值)不变地转发给另一个函数。这意味着如果原始参数是一个左值,它将被转发为左值;如果原始参数是一个右值,它将被转发为右值。完美转发通常涉及到两个函数模板:std::forwardstd::movestd::forward 模板函数用于转发参数,而 std::move 用于将左值转换为右值引用。

#include <iostream>
#include <type_traits>

using namespace std;
void printValue(int& value) 
{
    cout << "Lvalue reference to: " << value << endl;
}

void printValue(int&& value) 
{
    cout << "Rvalue reference to: " << value << endl;
}

template<typename T>
void forwardValue(T &&value) 
{
    printValue(std::forward<T>(value));
}

int main(void) 
{
    int x = 42;

    forwardValue(x);                    // 转发左值
    forwardValue(42);                   // 转发右值
    forwardValue(std::move(x));         // 强制转为右值并转发

    return 0;
}

  在这个示例中,forwardValue 是一个模板函数,它接受一个通用引用参数 T&&。使用 std::forward<T>(value),我们可以将参数 value 连同其值类别完美地转发给 printValue() 函数。这样,printValue() 函数就能根据接收到的参数是左值还是右值来选择正确的重载版本。

posted @ 2023-05-20 20:54  星光映梦  阅读(72)  评论(0编辑  收藏  举报