介绍一下右值引用和移动语义和完美转发

这篇文章介绍的了第三章中右值引用相关的内容。在介绍该内容之前,会对一些相关问题进行解释,便于理解后面的内容。 并且,提前说明,许多编译器会多拷贝构造和移动构造进行优化省略,这样就看不到拷贝构造和移动构造的过程,需要在编译器选项中设置-fno-elide-constructors来关闭优化。

指针成员和拷贝构造 
当一个类中含有指针成员时,由于默认的拷贝构造函数只会进行浅拷贝,所以当我们写出一下代码时:
 

class Base{
public:
    Base():data(new int(0)){}
    //Base(const Base& base): data(base.data){}  默认的拷贝构造
    ~Base()
    {
        std::cout << "~Base()" << std::endl;
        delete data;
    }

    void out()
    {
        std::cout << data << ":" << *data << std::endl;
    }
private:
    int* data;
};

int main()
{
    Base base1;

    {
        Base base2(base1);
        base1.out();       // address1:0
        base2.out();       // address2:0
    }                      // ~Base(),base2析构

    base1.out();           // address1:未知
    return 0;
}

由于base2只是拷贝了base1的指针,所以它们的指针地址是相同的,当base2析构之后,所指向的内存地址已经被释放,当base1再去调用的时候已经是野指针了,会造成未知后果。出现这种情况,我们只需要自己重新实现拷贝构造函数重新申请堆空间即可。

Base(const Base& base): data(new int(*base.data)){}

移动语义 
上一部分说到重新实现拷贝构造,但是这样还会一些问题,比如下面的例子:

Base getBase()
{
    return Base();       // 无参构造一次,临时变量拷贝构造一次
}

int main()
{
    Base a = getBase();  // 拷贝构造第二次

    return 0;
}

这样拷贝构造函数一共被调用了两次,申请空间又释放内存,效率比较低。我们可以通过在getBase传入引用来减少对内存的操作,但是有时候又希望能从返回值直接返回对象。C++11中新增了移动构造函数,可以避免这个问题:

Base(Base&& base): data(base.data){ base.data = nullptr; }

移动语义,即移动构造函数接受一个”右值引用”参数,可以暂时理解成临时变量的引用,在后面会进行说明。在移动构造函数中,将原来的指针成员指向nullptr,这样能够避免多次的申请释放。大家可能会有疑问:原来的指针指向nullptr之后,原来的类对象不就不能正常使用了么?这里就涉及到移动构造函数被触发的条件:用临时变量进行拷贝构造,才能触发移动构造。怎么养才会产生临时变量,我们需要先了解C++中的左值、右值和右值引用。 
需要注意的是,编译器默认会生成移动构造和拷贝构造函数,但是如果我们自己实现了移动构造、拷贝构造函数、赋值构造函数、析构函数中的任意一个,那么编译器就不会生成默认的移动构造和拷贝构造函数。如果只实现拷贝构造函数,那么这个类只有拷贝语义;如果只实现了移动构造函数,那么这个类只有移动语义。当我们不知道一个类是否可移动时,可以借助模板类来判断:std::is_move_constructible<T>::value;
左值、右值和右值引用 
典型情况下左值和右值可以通过赋值表达式中的位置进行判断,在等号左边的为左值、等号右边的为右值。下面的例子中,a就是左值,b + c就是右值。

a = b + c;

另外一个判别方法是:可以取地址、有名字的就是左值,否则就是右值。 
&a就符合左值的条件,而&(b + c)是不行的。再细分,右值又分为将亡值(expiring value)和纯右值(pure value)。 
纯右值主要有以下几种:

函数返回的非引用临时变量
运算表达式产生的临时变量值,比如上面的b + c
不和对象关联的字面量值,比如:1,‘a’,true
类型转换的返回值、lambda表达式
将亡值是C++11中新增的和右值引用有关的概念:将要被移动的对象,比如:函数返回值为T&&的返回对象、std::move的返回值(下面会说明)、类型转换为T&&的函数返回值(下面会说明)。 
由于右值通常不具备名字,所以只能通过右值引用来找到它。比如:
完美转发 
最后,终于要介绍完美转发了。完美转发是指:在函数模板中,完全按照模板参数的类型将参数传递给模板函数中调用的另一个函数,并且不产生额外开销。例如:

猜你喜欢

转载自blog.csdn.net/weixin_41066529/article/details/89977433