右值引用、移动语义、万能引用与完美转发

一、右值引用

1.右值与右值引用

在C++11中,右值分为两个概念:将亡值(xvalue, eXpiring Value)和纯右值(prvalue, Pure Rvalue)。

纯右值是C++98中右值的概念,表示用于辨识临时变量和一些不跟对象关联的值,临时变量如非引用返回的函数返回的临时变量值、一些运算表达式产生的临时变量的值,不跟对量关联的字面常量如:2‘c’true

将亡值是C++11新增的跟右值引用相关的表达式,将亡值可以理解为通过移动构造其他变量内存空间的方式获取到的值,在确保其他变量不再被使用、或即将被销毁时,来延长变量值的生命期。而实际上该右值会马上被销毁,所以称之为将亡值。

所谓右值引用就是必须绑定到优质的引用,右值引用的一个重要特性就是只能绑定到一个将要销毁的对象,因此使用右值引用的代码可以自由地接管所引用的对象的资源。

2.为什么有右值引用

假设定义了函数void foo(Test &t){...};,通过传递引用来提高效率,若以foo(Test())的形式调用函数,会编译不过,因为这是将引用绑定到一个匿名对象,它可能很快就不在了,所以没有意义。为了解决这一问题,定义重载函数void foo(Test t),仍会编译不过,因为这样当以foo(t)形式调用函数时,会有二义性。

引入右值引用可以解决上述问题,用void foo(Test&& t)void foo(Test& t)两个函数来区分传入值是否是将亡值,并且可以重载,无二义性。

3.左值引用与右值引用区别

左值引用是起别名,如果这个对象已经析构,那么这个别名也应该一起失效,也就是说左值引用一定要保证它的生命周期小于等于它被引用的对象。

当将亡值出现的时候,左值引用表示无能为力,所以右值引用出现了。

右值引用也可以看作起名,只是它起名的对象是一个将亡值,然后延续这个将亡值的生命,直到这个的右值的生命也结束了。

除了入参时可以用到右值引用外,其他右值引用都显得多余。

比如Test&& t{Test()};它和Test t{Test()}是一样的,甚至可以这样Test&& t{}它们都只一个普通对象,只调用一次构造函数。

二、移动语义

1.移动构造函数和移动赋值运算符

拷贝对象时在某些情况下,对象拷贝后就立即被销毁了,此时从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。另一方面,对于IO类、std::unique_ptr这样的类,他们都包含不能被共享的资源,因此,这些类型的对象不能拷贝但可以移动。

为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。

移动构造函数的第一个参数是该类类型的一个引用,且是一个右值引用。在移动构造函数中,获取了被移动对象的资源(这里是内存)的所有权;在接管内存之后,将给定对象中的指针都置为nullptr(使其成为析构安全地状态),这样就完成了从给定对象的移动操作。最终,移后源对象会被销毁,意味着在其上运行析构函数。

移动赋值运算符执行与析构函数和移动构造函数相同的工作。

2.std::move()

std::move(lvalue)的作用是把一个左值转换为右值。当局部变量的左值想移动而不是拷贝时,出现std::move将左值转为右值

// FUNCTION TEMPLATE move
template 
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
    return static_cast&&>(_Arg);
}

std::move是一个模板函数,通过remove_\reference_t获得模板参数的原本类型,然后把值转换为该类型的右值。使用std::move意味着,把一个左值转换为右值,原先的值不应该继续再使用。

三、万能引用

1.万能引用

C++ 11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值。但是注意了:只有发生类型推导的时候,T&&才表示万能引用;否则,表示右值引用。

template
void func(T&& param) {
    cout << param << endl;
}

int main() {
    int num = 2019;
    func(num);
    func(2019);
    return 0;
}

2.引用折叠

一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:

  • 左值-左值 T& &     -- T& # 函数定义的形参类型是左值引用,传入的实参是左值引用
  • 左值-右值 T& &&   -- T&  # 函数定义的形参类型是左值引用,传入的实参是右值引用
  • 右值-左值 T&& &   -- T&  # 函数定义的形参类型是右值引用,传入的实参是左值引用
  • 右值-右值 T&& &&  -- T&& # 函数定义的形参类型是右值引用,传入的实参是右值引用

但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。

四、完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
一个例子

template
void func(T& param) {
    cout << "传入的是左值" << endl;
}
template
void func(T&& param) {
    cout << "传入的是右值" << endl;
}

template
void warp(T&& param) {
    func(param);
}

int main() {
    int num = 2019;
    warp(num);
    warp(2019);
    return 0;
}

输出结果:

传入的是左值
传入的是左值

warp()函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;第一个warp()函数调用实参是左值,所以,warp()函数中调用func()中传入的参数也应该是左值;第二个warp()函数调用实参是右值,根据上面所说的引用折叠规则,warp()函数接收的参数类型是右值引用,但在warp()函数内部,因为参数有了名称,左值引用类型变为了右值,我们也通过变量名取得变量地址。

这就是我们所谓的“完美转发”技术,可以保持函数调用过程中,变量类型的不变,在C++11中通过std::forward()函数来实现。修改warp()函数如下:

template
void warp(T&& param) {
    func(std::forward(param));
}

五、总结

  • 右值引用可以解决传入的函数参数是将亡值的问题
  • 右值引用可能是左值也可能是右值,若这个右值引用被命名了,它就是左值
  • 移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值运算符
  • 若想对一个左值进行移动构造,可用std::move将一个左值转换成一个右值
  • 使用T&&类型的形参既能绑定右值,又能绑定左值。只有发生类型推导的时候,才表示万能引用;
  • 引用折叠规则:所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。
  • 通过一个函数将参数继续转交给另一个函数进行处理,如果继续保持参数的原有特征,就是完美转发。std::forward()和万能引用共同实现完美转发。

你可能感兴趣的:(右值引用、移动语义、万能引用与完美转发)