什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
i和j都可以被取地址,因此都是左值:
i和j都是左值引用:
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。
下面几个不可以被取地址,因此是右值:(右值可以是常数,表达式,函数):
以下几个为右值引用:
左值能否给右值取别名?
如下所示,不可以:
因为右值不能被修改,加引用就可以修改了,对右值来说,这是一种权力的返回广大,因此我们要加const:
右值表达式同理:
右值引用能否给左值取别名?
如下所示,不能:
但是右值可以给mov后的左值取别名:
左值引用使用场景和意义
(1)引用传参,(2)引用返回:
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
场景1:
我们模拟一个string,命名空间为bit。
bitstring.cc · 孙鹏宇/孙鹏宇的第一个仓库 - 码云 - 开源中国 (gitee.com)
接下来我们在模拟的string里写一个to_string函数(请先将bit命名空间里的移动构造函数,移动赋值函数注释掉):
场景1是没办法用左值引用返回的:
强行左值引用返回:
原因:
ret是to_string()的局部变量,出了作用域就会销毁,我们正常返回是对ret做了个拷贝,返回的是拷贝。如果强行用引用返回,相当于把ret的别名返回,ret出了作用域就销毁了,别名自然也销毁了。
只有出了作用域变量还在,才能用左值引用返回!
ret出了作用域就会销毁,所以需要拷贝构造创建一个临时变量,返回临时变量。
临时变量返回之后s去接受,又需要一次赋值拷贝。两次拷贝代价很大:
优化
ret对象无法拯救,但是ret指向的资源可以拯救!
右值分为普通右值和将忘右值,列如上文出了作用域就要销毁的就叫做将亡值。
对于将忘值,既然它都要快要灭亡了,那我们就直接拿它的资源来用,如下:
交换资源比做深拷贝代价要小。
右值插入
C++11给STL容器的 插入 接口提供了 右值插入版本,例如:
list:set:
等等。
什么情况下用左值插入,什么情况下用右值插入?
如下,list左值插入调用了一次深拷贝,list右值插入调用了两次移动构造,虽然是两次移动构造,但是代价也比一次深拷贝小:
如果没有移动构造函数,右值插入会有两次深拷贝:
接下来不用库的list,用我们自己模拟实现的list实现右值引用。
首先是以前写的没有右值引用功能的模拟实现list:list.h · 孙鹏宇/孙鹏宇的第一个仓库 - 码云 - 开源中国 (gitee.com)
在原先模拟实现的list基础上增加 C++11右值插入版本:
我们先用库的list指行如下操作:
(1)插入左值s1,(2)调用to_string,(3)插入字符串"222222"。
发现,只有左值插入时才会产生深拷贝,剩下的全是调移动构造:
然后我们调自己写的list指向同样的操作:
发现只调了一次移动构造,剩下全是深拷贝!
这是什么原因呢?
通过调试,发现调用to_string确实调用的是移动构造和右值尾插:
然后我们可以看一下 insert调的是哪个版本的insert()函数。F11进入insert()函数,如下:
我们发现竟然调了左值插入。
总结
右值不能修改,但是右值的引用可以修改(右值的引用本质是调用移动拷贝,如果右值引用不能被修改,怎么转移资源?),即右值的引用为左值:
这样就可以解释为什么最后调到了 左值插入:
解决方法:
我们可以move一下,就可以调 右值插入了:
运行,发现还是只调了一次移动构造:
原因:
insert()会把x传给new,new会调构造函数,构造函数是左值引用:
所以我们还需要再写一个右值引用版本的构造函数:
不能写两个全缺省的构造函数,编译器不知道调哪个,我们把右值引用全缺省构造函数改一下:
原因:
右值引用的属性是左值,我们需要再move一下把它变回右值:
完美转发
模板中的&& 万能引用
传统的引用来看这个是右值引用,但实际上它既可以是左值引用又可以是右值引用,因为它是模板
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
写一段如下代码:
传参调用一下:
结果:
为什么全是左值引用?
因为右值的引用是左值,那么move一下呢?
也不行,我们期望保持值原本的属性,即:是左值就调左值引用,是右值就调右值引用
这个时候我们就要用到完美转发这个东西:
代码托管
右值引用和移动语义 · 孙鹏宇/孙鹏宇的第一个仓库 - 码云 - 开源中国 (gitee.com)