深入解读C++中的右值引用

2020-01-06 15:15:48刘景俊
通过常量左值引用,可以延长ReturnValue()返回值的生命周期,但是不能修改它。C++11的右值引用出场了:

A&& a3 = ReturnValue();

右值引用通过”&&”来声明, a3引用了ReturnValue()的返回值,延长了它的生命周期,并且可以对该临时值进行修改。

二. 移动语义
右值引用可以引用并修改右值,但是通常情况下,修改一个临时值是没有意义的。然而在对临时值进行拷贝时,我们可以通过右值引用来将临时值内部的资源移为己用,从而避免了资源的拷贝:


#include<iostream>

class A
{
public:
 A(int a)
 :_p(new int(a))
 {
 }

 // 移动构造函数 移动语义
 A(A&& rhs)
 : _p(rhs._p)
 {
 // 将临时值资源置空 避免多次释放 现在资源的归属权已经转移
 rhs._p = nullptr; 
 std::cout<<"Move Constructor"<<std::endl;
 }
 // 拷贝构造函数 复制语义
 A(const A& rhs)
 : _p(new int(*rhs._p))
 {
 std::cout<<"Copy Constructor"<<std::endl;
 }
 
private:
 int* _p;
};

A ReturnValue() { return A(5); }

int main()
{
 A a = ReturnValue();
 return 0;
}

运行该代码,发现Move Constructor被调用(在g++中会对返回值进行优化,不会有任何输出。可以通过-fno-elide-constructors关闭这个选项)。在用右值构造对象时,编译器会调用A(A&& rhs)形式的移动构造函数,在移动构造函数中,你可以实现自己的移动语义,这里将临时对象中_p指向内存直接移为己用,避免了资源拷贝。当资源非常大或构造非常耗时时,效率提升将非常明显。如果A没有定义移动构造函数,那么像在C++98中那样,将调用拷贝构造函数,执行拷贝语义。移动不成,还可以拷贝。
std::move:
C++11提供一个函数std::move()来将一个左值强制转化为右值:


A a1(5);
A a2 = std::move(a1);

上面的代码在构造a2时将会调用移动构造函数,并且a1的_p会被置空,因为资源已经被移动了。而a1的生命周期和作用域并没有变,仍然要等到main函数结束后再析构,因此之后对a1的_p的访问将导致运行错误。
std::move乍一看没什么用。它主要用在两个地方:
  • 帮助更好地实现移动语义
  • 实现完美转发(下面会提到)

    考虑如下代码:

    
    class B
    {
    public:
     B(B&& rhs)
     : _pb(rhs._pb)
     {
     // how can i move rhs._a to this->_a ?
     rhs._pb = nullptr;
     }
    
    private:
     A _a;
     int * pb;
    }