简介

关于C++中的右值引用的详细可以看这一批博文《​​从4行代码看右值引用​​》。那一篇博文详细结合四行简单的代码详细介绍了右值引用的使用方法和一些场景,非常实用。

而本篇博文主要介绍一下我在学习右值引用的一些心得。因为在学习右值引用的时候,有一些地方非常难理解。所以写下这一篇博文,防止遗忘,由于对于C++涉猎不多,所以有一些不正确的地方,欢迎斧正。

为什么要使用右值引用?

对于普通开发者来说,右值引用的好处在于可以将保证在取用函数返回值的时候,减少一次复制的机会。如:

#include <iostream>
using namespace std;

class Test
{
private:
public:
Test(/* args */) {
cout << "构造" << endl;
}
int a = 0;
~Test() {
cout << "析构" << endl;
}

Test(const Test &a) {
cout << "复制" << endl;
}
};

Test GetTest() {
Test af;
return af;
}

int main() {
Test a = GetTest();
return 0;
}

使用编译选项-fno-elide-constructors,可以得到结果

构造
复制
析构
复制
析构
析构

可以发现,为了保证正常运行和堆栈正常,程序对Test对象经过了一次构造和两次复制。分别是


  1. ​GetTest()​​​函数中​​Test af​​​的构造,由于af是​​GetTest()​​​的临时变量,因此此时使用的内存空间是​​GetTest()​​里面的栈1
  2. 为了把af从​​GetTest()​​​的栈中取出来,于是将把af指向的Test对象复制到了​​main()​​的栈,由于程序“并不知道”接下来的操作,所以此时对象的存在就是一个临时的。语句执行完,它就析构了。
  3. 为了保存这一个对象,我们使用了a,而为了将上一步得到的值复制到a,程序又进行了一次复制。

以上便是程序的全过程。而可以很清楚的发现,第2次复制得到的值和第3次复制得到的值,它们都位于​​main()​​​的堆栈空间内的,他们的生命周期都是一样的,和​​main()​​一样长。那么我们有没有办法可以减少第3次的复制呢?

在没有右值引用的时候有两种方法。


  1. 靠编译器优化2
  2. 使用​​const Test&​​,常量引用。

第一种方法不表,以目前的编译器优化,的确可以获得非常多的优惠,但是这些编译器提供的,并不是语法提供的,不同的编译器可能存在不一样的表现。第二种在需要改变返回值的时候非常麻烦。

而右值引用可以解决这一问题。我们将​​main()​​函数的代码改为

int main() {
Test &&a = GetTest();
return 0;
}

编译运行后得到额结果为

构造
复制
析构
析构

可以看到,程序减少了一次复制。这一次的减少就是上述的第3次的复制。而且此时的变量a可以随意的更改。

那我们可以把第二次的复制也去掉吗?当然可以,比如把​​GetTest()​​​的返回值改为​​Test&&​​​,然后给af加上​​std::move​​强制转换成右值。但是,这样的操作破坏了程序的堆栈,会出现很大的问题。

右值引用和unique_ptr

​unique_ptr​​​是C++中智能指针的一部分,另外还有​​auto_ptr​​​和​​shared_ptr​​​。但是刚刚使用C++不久,所以只对​​shared_ptr​​​有一定的了解,而​​auto_ptr​​​由于一些原因已经被弃用了。​​unique_ptr​​​只允许一个智能指针对象拥有指针。而​​shared_ptr​​​则允许多个,其内部有一个引用计数器,当引用为0时便释放空间,因此相对于​​unique_ptr​​,其所占的内存空间较大,使用的时候,效率也相对较低。

​unique_ptr​​​的实现必须保证不共享其指针,不复制到其他的​​unique_ptr​​,也不能使用值传递到函数里面。要将其所有权交给其他变量,只能通过“移动”的操作。

为了保证移动的语义上的正确性,被移动的​​unique_ptr​​​必须为右值。因为右值是被认为是即将消亡的值(将亡值),所以移动操作可以把被移动的​​unique_ptr​​的引用值为nullptr。

std::move和std::forward

​std::move​​​是将一个左值转换为右值,在内部实现直接使用了强制转换​​static_cast<T&&>()​​​。它一般用于将左值转换为右值,在​​unique_ptr​​转移的时候需要使用。一般来说,使用了move后,该对象就不应该继续使用了。

​std::forward​​即完美转发,保证参数在传递过程中属性不变,右值还是右值,左值还是左值。

附录


  1. 此处表达可能有一些不正确,因为所有的函数调用及其局部变量应该都是在一个栈上。但是在函数结束的时候,必须要恢复栈的指针。这一步便需要把其局部变量全部删除,因此可以认为其局部变量是位于函数的栈中。
  2. 去掉编译选项-fno-elide-constructors
    ------------恢复内容结束------------