左值与右值
C++中左值与右值的概念是从C中继承而来,一种简单的定义是左值能够出现再表达式的左边或者右边,而右值只能出现在表达式的右边。
int a = 5; // a是左值,5是右值
int b = a; // b是左值,a也是左值
int c = a + b; // c是左值,a + b是右值
另一种区分左值和右值的方法是:有名字、能取地址的值是左值,没有名字、不能取地址的值是右值。比如上述语句中a,b, c是变量可以取地址,所以是左值,而5和a + b无法进行取地址操作,因此是右值。C++中左值与右值的一个主要的区别是:左值可以被修改,而右值不可修改。
左值引用与右值引用
了解了左值与右值的概念后,接下来介绍下C++中的左值引用与右值引用。左值引用很简单,就是一个变量的别名,绑定到一个左值上:
int a = 1;
int& b = a; //a = 1,b = 1
b = 2; // a = 2,b = 2
这里b就等于a,在汇编层面其实和普通的指针一样,对引用的修改(b)也会修改到被引用的对象(a),需要注意的是,因为引用实际是一个别名,因此必须初始化,即告诉编译器是那个具体对象的别名。因此下列左值引用都是错误的:
int& a; // 错误!左值引用必须初始化
int& b = 10; // 错误!左值引用不能以临时变量初始化(临时变量没有地址)
右值引用是C++11中新增的特性,顾名思义,右值引用就是用来绑定到右值的引用,一个右值被绑定到右值引用之后,原本需要被销毁的此右值生命周期会延长至绑定它的右值引用的生命周期。在汇编层面,右值引用和const引用所做的事情是一样的,即产生临时量来存储常量。但是右值引用可以进行读写操作,而const引用只能进行读操作。绑定右值引用使用&&,具体使用如下:
int a = 5;
int& b = a; // 正确!b是一个左值引用
int&& c = 6; // 正确!c是一个右值引用,绑定到右值6
int&& d = a * 2; // 正确!d是一个右值引用,绑定到右值a * 2
int&& e = i; // 错误!不能将左值绑定到右值引用
int& f = 7; // 错误!不能将右值绑定到左值引用
const int& g = a * 3; // 正确!可以将右值绑定到const 左值引用
可以看到我们虽然不能将右值绑定到左值引用,但是可以将右值绑定到const左值引用。
注意: 变量表达式都是左值!。变量可以看作是只有一个运算对象而没有运算符的表达式,跟其他表达式一样,变量表达式也有左值/右值属性。变量表达式都是左值,因此我们不能将一个右值引用绑定到一个右值引用类型的变量上。
int&& a = 5; // 正确!a是一个右值引用
int&& b = a; // 错误!a是一个左值,不能绑定到右值引用
这里虽然a是右值引用类型,但是确实一个左值,因此无法绑定到右值引用b上。因为在C++中,右值一般是临时对象,但是绑定到右值引用之后,其生命周期变长了,因此a是一个左值。我们不能将一个右值引用直接绑定到一个变量上,即使是这个变量是右值引用类型也不行。具体的这个问题在后续的介绍forward的时候会详细说明。
左值/右值引用的模板实参推断
在另一篇文章中介绍了C++的模板类型推断的几种类型,可以总结为以下三种:
- ParamType 是一个指针或者引用(&),但是不是通用引用(&&)
- ParamType是一个通用引用(&&)
- ParamType既不是指针,也不是引用(&)或者通用引用(&&)
从左值引用函数参数推断类型
当一个函数参数是模板的左值引用(T&)时,根据绑定规则,只能传递一个左值实参,这个左值实参可以时const类型,也可以不是。如果实参时const的,那么T就会被推导为const类型
template<typename T>
void func(T& param);
int a = 0;
const int b = a;
func(a); // T被推导为int,param类型为int&
func(b); // T被推导为const int,param类型为const int&
func(5); // 错误!实参必须是一个左值!
如果一个函数的类型时const T&,那么根据绑定规则,可以传递任何类型的实参:const或者非const,左值或者右值,由于函数类型本身已经是const,因此T的推导结果不会是一个const,因为const已经是函数参数类型的一部分了。
template<typename T>
void func(const T& param);
int a = 0;
const int b = a;
func(a); // T被推导为int,param类型为const int&
func(b); // T被推导为int,param类型为const int&
func(5); // 正确!const T&可以绑定一个右值,T为int
可以看到,当函数参数类型为const T&时,可以接受一个右值实参,而函数参数类型为 T& 时是不可以的。
从右值引用函数参数推断类型
当一个函数的参数是一个右值引用(T&&)时,根据绑定规则可以传递一个右值实参。类似左值引用推导,右值引用推导得到的T的类型为右值的类型:
template<typename T>
void func(T&& param);
func(5); // 实参5为右值,T被推导为int类型
与不能给右值引用赋值左值不同,右值引用函数的模板实参却可以接受一个左值的输入。当我们将一个左值传递给函数的右值引用参数时,且此右值引用指向模板参数类型(T&&)时,编译器推导模板类型参数为实参的左值引用类型:
template<typename T>
void func(T&& param);
int a = 1;
func(a); // T被推导为int&,而不是int
如上述推导所示,当传入一个左值a时,T被推导为int&,而不是int,对应的param的类型为int& &&,根据引用折叠的规则,int& &&被折叠为int&。
引用折叠规则
T& & ,T& && 和T&& &都会被折叠为T&
T&& &&被折叠为T&&
引用折叠的规则告诉我们:如果一个函数的参数时指向模板参数类型的右值引用(如T&&),则可以传递给它任意类型的实参,如果传递的左值实参,那么T将会推导成为一个左值引用,函数参数被实例化为一个普通的左值引用(T&)。这种引用叫做“通用引用”。
右值引用与通用引用
C++中T&&有两种不同的意思,第一种是右值引用,用于绑定到右值上,它们主要存在的原因是为了声明