1、左值和右值
左值(left-values),缩写:lvalues ,located value 可定位值,其含义是可以明确其存放地址的值,更确切说对其的使用是基于地址
右值(right-values),缩写:rvalues , read value 可读的值,通常指代赋值运算=右侧的常量值,字面值,或者函数的返回值,它们没有具体的指代名,即无法通过地址访问,通常在赋值表达式结束后变销毁。
一般可以认为:左值对应变量的地址,右值对应变量的值,首先说左值和右值,他们绝不是简单的等号左边和右边的区别,总结来说:
- 左值可以寻址,而右值不可以。
- 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
- 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。
int value=fun();
最后c++11中还有一个将亡值的概念,是c++11中新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象
2、左值引用和右值引用
左值引用是对左值的引用类型,用 T & a 来表示
右值引用是对右值的引用类型,用 T &&a 来表示
左值引用和右值引用同为引用,他们在声明的同时必须被初始化(引用语法规定)
不要混淆 取地址 和 引用,当&说明符前面带有类型声明,则是引用,否则就是取地址(必须是声明以及初始化),通俗来说 &在 ”=” 号左边的是引用,右边的是取地址。
左值引用:也就是通常所述的引用,引用是原先左值的别名,在定义时完成初始化,并且不可以作变更。左值引用和左值共同使用内存中同一份内容数据。
const int&a = 1 //常量左值引用是可以绑定右值的
const int a = 1 //从语法上讲后者的右值在表达时结束后就销毁了,而前者不会
函数在传值方式传递参数,以及返回函数值(例如返回类型为复杂类类型(class)),都会执行拷贝构造,将带来大量无畏的拷贝构造开销。
这就是我们尽量用const 引用代替值传递做函数参数的原因,它在某种程度上可以提高效率。
右值引用:即(Move Semantics:移动语义),避免无意义的拷贝赋值操作,C++11提出右值引用的概念,其本质是接替右值的所有权(不销毁原先的内存内容,而是将所有权移交给被交付的对象。)。
int &&value = 1 ;//右值引用一个常量值
int &&value = fun();//右值引用一个临时的函数返回值
- 相对于左值,右值的生命周期很短,如函数的临时返回值,可以安全的转移控制权。
- 将右值的资源不释放,而是采取右值引用的方式继续使用,减少大量的拷贝,复制带来的开销。
- 编译器会默认开启返回值优化,解决重复对象构造问题,而采取右值引用可以语言层面实现。
C++11引入的右值引用带来了深刻的性能提升,改变了以往复制,拷贝的繁琐操作,转而采取更为智能的移动技术,其本质通过移交所有权,在不改变内存内容情况下,完成资源的转移。
例如源码STL中的,vector的扩容使用到了右值引用来提高效率:
当Vector的size增大到capacity的一定比例后,需要申请一片更大的内存空间,同时将原先的数据转移到新空间。在移动技术之前,这意味着大量元素的深拷贝,而采取移动技术,无需释放原先空间,而只需要将原先空间所有权交由给新空间创建者即可。
额外知识点深拷贝、浅拷贝:https://blog.csdn.net/u014430031/article/details/115383480
总结生面两段话,左值引用和右值引用座位函数参数都能避免对象的拷贝和构造。
但是我们通过右值引用改变一个右值时是没有意义的,而我们通过左值引用改变一个左值是有意义的。
3、Perfect Forwarding:完美转发(move、forward函数)
实际上std::move就是一个类型转换器,将左值转换成右值而以。我们来看一下它的实现吧!
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_case<typename remove_reference<T>::type&&>(t);
}
通用引用
首先我们来看一下move的输入参数,move的输入参数类型称为通用引用类型。什么是通用引用呢?就是它既可以接收左值也可以接收右值。
代码中有两种类型的通用引用: 一种是auto,另一种是通过模板定义的T&&。实际上auto就是模板中的T,它们是等价的。
模板的类型推导
通用引用好强大呀!它既可以接收左值又可以接收右值,它是如何做到的呢?这就要讲讲模板的类型推导了。
代码中分为两种类型:基本类型和引用类型。
基本类型:不带地址的普通原始类型。
引用类型:存储的值是地址,指向存放数据的位置。
模板的类型推导规则还是蛮复杂的,这里我们只简要说明一下,有兴趣的同学可以查一下C++11的规范。我们还是举个具体的例子吧:
template <typename T>
void f(T param); //第一类
//下面是传值的模板,由于传入参数的值不影响原值,所以参数类型退化为原始类型
int x = 10; // x是int
const int& rx = x; // rx是const int &
f(rx); // T是int
对于第一类其推导时根据的原则是,函数参数传值不影响原值,所以无论你实际传入的参数是普通变量、常量还是引用,它最终都退化为不带任何修修饰的原始类型。
template <typename T>
void func(T& param); //第二类
template <typename T>
void function(T&& param); //第三类
//下面是传引用模板, 如果输入参数类型有引用,则去掉引用;如果没有引用,则输入参数类型就是T的类型
int x = 10; // x是int
const int& rx = x; // rx是const int &
func(x); // T为int
func(rx); // T为const int
//下面是通用引用模板,与引用模板规则一致
function(x); // T为int&
function(5); // T为int
第二类为模板类型为引用(包括左值引用和右值引用)或指针模板。这一类在类型推导时根据的原则是去除对等数量的引用符号,其它关键字照般。还是我们上面的例子,func(x)
中x的类型为 int&
,它与T&
放在一起可以知道T为int。另一个例子function(x)
,其中x为int&
它与T&& 放在一起可知T为int&
。
返回类型
我们来看一下move的remove_reference看着很陌生,接下来我们再分析一下remove_reference类,看它又起什么作用吧。其实,通过它的名子你应该也能猜个大概了,就是通过模板去除引用。我们来看一下它的实现吧。
template <typename T>
struct remove_reference{
typedef T type; //定义T的类型别名为type
};
template <typename T>
struct remove_reference<T&> //左值引用
{
typedef T type;
}
template <typename T>
struct remove_reference<T&&> //右值引用
{
typedef T type;
}
通过上面的代码我们可以知道,经过remove_reference处理后,T的引用被剔除了。假设前面我们通过move的类型自动推导得到T为int&&,那么再次经过模板推导remove_reference的type成员,这样就可以得出type的类型为int了。
remove_reference利用模板的自动推导获取到了实参去引用后的类型。现在我们再回过来看move函数的时候是不是就一目了解了呢?之前无法理解的5行代码现然变成了这样:
int && move(int&& && t){
return static_case<int&&>(t);
}
//或
int && move(int& && t){
return static_case<int&&>(t);
}
经上面转换后,我们看这个代码就清晰多了,从中我们可以看到move实际上就是做了一个类型的强制转换。如果你是左值引用就强制转换成右值引用。
引用折叠
上面的代码我们看起来是简单了很多,但其参数int& &&
和int && &&
还是让人觉得很别扭。因为C++编译器根本就不支持这两种类型。咦!这是怎么回事儿呢?
查看一下引用折叠规则:
|
|
Expanded type |
Collapsed type |
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
总结一句话就是左值引用总是折叠为左值引用,右值引用总是折叠为右值引用。
forward的作用
std::forward被称为完美转发,它的作用是保持原来的值
属性不变。啥意思呢?通俗的讲就是,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。
forward实现原理
要分析forward实现原理,我们首先来看一下forward代码实现。由于我们之前已经有了分析std::move的基础,所以再来看forward代码应该不会太困难。
……
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
return static_cast<T&&>(param);
}
……
forward实现了两个模板函数,一个接收左值,另一个接收右值。在上面有代码中:
typename std::remove_reference<T>::type
的含义我们在分析std::move时已经向你做了说细的说明,其含义就是获得去掉引用的参数类型。所以上面的两上模板函数中,第一个是左值引用模板函数,第二个是右值引用模板函数。
紧接着std::forward模板函数对传入的参数进行强制类型转换,转换的目标类型符合引用折叠规则,因此左值参数最终转换后仍为左值,右值参数最终转成右值。
总结:
右值引用将左值与右值区分开来。它们可以帮助您通过消除不必要的内存分配和复制操作来提高应用程序的性能。它们还使您能够编写接受任意参数的函数的一个版本,并将其转发给另一个函数,就好像直接调用了另一个函数一样