c++11,实用方法介绍(二)——右值引用

        

目录

五、右值引用

1、左值与左值引用

2、右值与右值引用

3、左值引用和右值引用的比较

4、 左右值引用的意义

(1)、问题提出——局部对象做返回值

(2)、左值引用做返回值

(3)、右值引用做返回值

(4)、利用传值返回

(5)、c++98对传值返回的优化

(6)、c++11借助右值引用解决局部对象做返回值

<1>补充知识:

<2>解决方法:

<3>进一步优化

(7)、右值在赋值场景的应用

(8)、右值引用意义总结

5、右值引用在容器插入的应用

6、完美转发

forward

 结束语:


        书接博客《c++11,实用方法介绍(一)》,我们继续来介绍c++11中的实用方法。上篇博客讲到新增的容器插入接口(右值引用)版本,移动拷贝,移动赋值等非常有价值和意义,下面让我们一起来,一步一步地深入理解右值引用及其应用

五、右值引用

1、左值与左值引用

        左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。下图为相关举例参考。

注意:

        上图中,const int 变量地址由于windows的防御机制,不可以使用cout打印,可以通过监视查看,或者利用printf打印

2、右值与右值引用

        右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。下图为相关举例参考。

        需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,利用rr1右值引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用

3、左值引用和右值引用的比较

1. 左值引用只能引用左值,不能引用右值。但是const左值引用既可引用左值,也可引用右值

2. 右值引用只能引用右值,不能引用左值。但是右值引用可以move以后的左值

4、 左右值引用的意义

(1)、问题提出——局部对象做返回值

Q:左值引用解决了什么问题?

(1)、传参的拷贝的问题被全解决了

(2)、传返回值的问题解决了一部分(非局部变量,退出作用域后仍存在的变量,可以左值引用返回,减少了拷贝)

Q:没有解决的问题?

        局部对象(出了作用域就销毁的对象),返回的拷贝问题,没        有解决。

(2)、左值引用做返回值

        局部对象被销毁,数据也被清空了,硬要引用被释放的空间,会导致越界访问且没有数据

(3)、右值引用做返回值

        右值引用不是直接起作用,临时对象是左值,也不能对其直接右值引用(move以后也不行,没有解决临时对象依旧被销毁的问题),下图为错例

(4)、利用传值返回

        面对没有解决的问题,以前只能使用传值返回:局部对象做返回值,连续两次拷贝构造。第一次拷贝构造为临时对象(将亡值),第二次拷贝给main()中对象,有的时候吗,该对象可能是一个数据结构套着一个数据结构的对象,也有可能是一棵红黑树,哈希表,等等这样结构的对象,对于这类深拷贝的对象,他们的拷贝构造的代价是极大,最终导致效率很低。见下图左

补充:当临时对象比较小的时候存在寄存器,比较大的时候存在中,介于main栈帧与函数调用栈帧之间

(5)、c++98对传值返回的优化

编译器层面的优化见上图右        

        将两次拷贝构造,优化为一次拷贝构造,在销毁局部对象之前拷贝,从而不开出存放临时对象的空间,简而言之就是不拷贝构造临时对象,前提是写在一起才会优化,不然就是赋值+拷贝构造,不会被优化。如下图

        虽然c++98已经做了不小的努力来减少拷贝,但是也仍然有一次拷贝构造,对于需要深拷贝的对象,拷贝代价大的问题仍然存在。这也就是为什么要有右值引用的原因

(6)、c++11借助右值引用解决局部对象做返回值

<1>补充知识:

(1)c++11 对右值概念的解释,细分

1、纯右值:内置类型的右值,如:10/a+b

2、将亡值:自定义类型的右值,如:匿名对象,传值返回函数/obj++/to_string(123)

(2)左右值引用的匹配问题

以前,左右值参数都会走左值引用参数,那是因为没有对应右值引用参数的对应函数的重载,如果增添了右值引用参数的函数,那么还是最匹配原则,各自走各自更加匹配的函数。

<2>解决方法:

很明显对函数中局部对象做返回的值,该临时对象是一个即将被释放的对象,返回它时,它会拷贝构造出一个临时对象,我们将其看作为将亡值,对于这类右值,如果我们采取深拷贝,效率很低,在这之后,被拷贝的临时对象又会被释放。对于重复的资源复制又删除,不如将要删除的资源提前转移给要复制这些资源的对象,即移动拷贝:进行资源交换,把空换给将亡的临时对象。(资源空间越大,效率提升越明显)

在对应的类中增加移动构造 参数为右值引用,供将亡值匹配,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

c++提供右值引用,本质就是为了参数匹配时区分左值和右值,对于不同类型,匹配更加适合的拷贝构造,避免对(将亡值)深拷贝的浪费。

<3>进一步优化

1、将一次拷贝构造(将亡值)+一次移动构造优化为一次移动构造

2、隐式的强行对返回的局部对象move(),转成右值

解释:

        移动构造是以右值引用为参数的函数,优化后,省略了中间构建将亡值的过程,直接对局部对象移动构造,就需要利用move(),将局部对象转成右值,才可以调用到移动构造。

但是如果显示的调用move,会导致以前写好的大量c++工程都要被迫改动,无法直接兼容以前,这样不太好,于是采用隐式的move(局部对象),之前的代码也不用改变。

总结:

1、移动构造只是右值引用的一个应用

2、不是所有的类都需要移动构造

        深拷贝的类需要移动构造,浅拷贝的类不需要移动构造(浅拷贝的类没有可以用于转移的资源)

3、右值引用的移动语义,不延长对象的生命周期,而是延长资源的生命周期

4、将右值引用写为参数时,一般不加const,除非希望右值不被掠夺资源。

(7)、右值在赋值场景的应用

        需要重载,右值引用为参数的赋值函数(移动赋值拷贝)

        赋值表达式一般是先根据返回值生成一个临时对象,再进行赋值操作。

对于左值,如果是用一个已经存在的对象接收,编译器就没办法优化了

老实实进行一次深拷贝+一次赋值深拷贝

对于右值(将亡值),交换资源,并且借助将亡值释放掉旧的空间,即移动赋值拷贝。由于强行隐式move,返回值编译器自动识别为右值,直接调用移动构造生成临时对象,再进行移动拷贝。最终为一次移动构造,一次移动拷贝

(8)、右值引用意义总结

1、左值引用没有解决的问题,右值引用解决了。深拷贝对象传值返回只需要移动资源,代价很低。

2、c++11开始,所有容器都增加了移动构造和移动赋值,大大提高了拷贝效率

5、右值引用在容器插入的应用

在容器的插入函数中也提供了,以右值应用为参数的重载插入函数

对比存在的对象插入,匿名对象插入,隐式转化插入

        所谓插入就是将对象拷贝进入容器。

        存在的对象插入是对左值的拷贝,很明显需要进行深拷贝。

        后两种插入是将临时对象的内容拷贝进容器节点空间,临时对象都是将亡值,是右值,自动调用移动拷贝,直接转移对应的资源。

注意:

1、左值move以后,资源可能会被转移掠夺,不要轻易的对左值move

2、右值被右值引用后,右值引用的属性是左值,所以在连续调用的函数中如果希望参数一直是右值,就要在传递参数前move()

Q:为什么按照第二点这样设计?

        我们知道右值是无法改变的,右值引用后右值引用具有左值属性,那么就是可以改变的,这样便于转移右值的资源

6、完美转发

 补充:模板的万能引用

        在模板中希望,模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。

1、不能单纯把模板的&&理解成右值引用的模板

2、 万能引用-> 传左值,他就是左值引用

3、万能引用-> 传右值,他就是右值引用

优点:代码复用,如果不利用万能引用的化,就需要4个参数引用的模板的模板,&, const &,&&,const &&

举例:万能模板的使用,及模拟推演实例化(如下图)

forward<T>

        在右值参数逐层传递的过程中,右值引用接收数据,会让其具有左值属性,再次传递是当作左值传递给下一层。这样导致我们无法调用到后面参数为右值引用的函数,而调用的是以左值引用为参数的函数,达不到预期目的。有以下解决方法

1、传参前move()一下将其转换为右值的方法

2、forward 完美转发在传参的过程中保留对象原生类型属性

forward,保持属性,本身是左值,保持不变,本身是右值,右值引用后,属性变为左值,在转变为右边值(相当于move了一下),move则是是左属性转右属性,foward相当于加了一层判断。

 结束语:

        本篇文章的内容就到此结束了,希望通过这篇博客大家能有所收获,有什么内容不明白的,大家可以在评论去向我提问,我会一一回答,当然有什么错误或者有什么不足的地方,希望大家可以包容并指出,后续还会更新c++11的其他使用方法,在博客《c++11,实用方法介绍(三)》中,希望大家可以持续关注,向每一位读者送上真诚的小花。

  • 40
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值