目录
背景
观察以下代码段:
void fun(int &x)
{
//
}
int main() {
fun(10);
return 0;
}
在编译的时候,会提示如下
error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
这个报错比较明显,10是右值,函数的参数是左值引用,所以肯定会报错,本次主要让大家来区分左值、纯右值和将亡值
C++11之前,左值遵循了C语言的分类法,但与C不同的是,其将非左值表达式统称为右值,函数为左值,并添加了引用能绑定到左值但唯有const的引用能绑定到右值的规则。几种非左值的C表达式在C++中成为了左值表达式
自C++11开始,对值类别又进行了详细分类,在原有左值的基础上增加了纯右值和消亡值,并对以上三种类型通过是否具名(identity)和可移动(moveable),又增加了glvalue和rvalue两种组合类型,在后面的内容中,会对这几种类型进行详细讲解
表达式
C/C++代码是由标识符、表达式和语句以及一些必要的符号(大括号等)组成。
表达式由按照语言规则排列的运算符,常量和变量组成。一个表达式可以包含一个或多个操作数,零个或多个运算符来计算值。每个表达式都会产生一些值,该值将在赋值运算符的帮助下分配给变量。
在C/C++中,表达式有很多种,我们常见的有前后缀表达式、条件运算符表达式等。字面值(literal)和变量(variable)是最简单的表达式,函数的返回值也被认为是表达式。
表达式是可求值的,对表达式求值可得到一个结果,这个结果有两个属性:
- 类型。这个我们很常见,比如int、string、引用或者我们自定义的类。类型确定了表达式可以进行哪些操作。
- 值类别
值类别
在C++11之前,表达式的值分为左值和右值两种,其中右值就是我们理解中的字面值1、true、NULL等。
自C++11开始,表达式的值分为左值(lvalue, left value)
、将亡值(xvalue, expiring value)
、纯右值(pvalue, pure ravlue)
以及两种混合类别泛左值(glvalue, generalized lvalue)
和右值(rvalue, right value)
五种。
这五种类别的分类基于表达式的两个特征:
- 具名(identity):可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址
-
可被移动:移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式
结合上述两个特征,对五种表达式值类别进行重新定义:
-
lvalue:具名且不可被移动(左值)
-
xvaue:具名且可被移动(将亡值)
-
prvalue:不具名且可被移动(纯右值)
-
glvalue:具名,lvalue和xvalue都属于glvalue(泛左值)
-
rvalue:可被移动的表达式,prvalue和xvalue都属于rvalue(右值)
左值
左值具有以下特征:
-
可通过取地址运算符获取其地址
-
可修改的左值可用作内建赋值和内建符合赋值运算符的左操作数
-
可以用来初始化左值引用
那么哪些都是左值呢?如下所示(大家可以背一背)
-
变量名、函数名以及数据成员名
-
返回左值引用的函数调用
-
由赋值运算符或复合赋值运算符连接的表达式,如(a=b, a-=b等)
-
解引用表达式*ptr
-
前置自增和自减表达式(++a, ++b)
-
成员访问(点)运算符的结果
-
由指针访问成员(
->
)运算符的结果 -
下标运算符的结果(
[]
) -
字符串字面值("abc")
int a = 1; // a是左值
T& f();
f();//左值
++a;//左值
--a;//左值
int b = a;//a和b都是左值
struct S* ptr = &obj; // ptr为左值
arr[1] = 2; // 左值
int *p = &a; // p为左值
*p = 10; // *p为左值
class MyClass{};
MyClass c; // c为左值
"abc"
纯右值
以下表达式的值都是纯右值:
-
字面值(字符串字面值除外),例如1,'a', true等
-
返回值为非引用的函数调用或操作符重载,例如:str.substr(1, 2), str1 + str2, or it++
-
后置自增和自减表达式(a++, a--)
-
算术表达式
-
逻辑表达式
-
比较表达式
-
取地址表达式
-
lambda表达式
nullptr;
true;
1;
int fun();
fun();
int a = 1;
int b = 2;
a + b;
a++;
b--;
a > b;
a && b;
将亡值
将亡值(xvalue, expiring value),顾名思义即将消亡的值,是C++11新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。(通过右值引用来续命)。
xvalue 只能通过两种方式来获得,这两种方式都涉及到将一个左值赋给(转化为)一个右值引用:
-
返回右值引用的函数的调用表达式,如
static_cast<T&&>(t);
该表达式得到一个 xvalue -
转换为右值引用的转换函数的调用表达式,如:std::move(t)
std::string fun() {
std::string str;
// ...
return str;
}
std::string s = fun();
在函数fun()中,str是一个局部变量,并在函数结束时候被返回。
在C++11之前,s = fun();会调用拷贝构造函数,会将整个str复制一份,然后把str销毁。如果str特别大的话,会造成大量额外开销。在这一行中,s是左值,fun()是右值(纯右值),fun()产生的那个返回值作为一个临时值,一旦str被s复制后,将被销毁,无法获取、也不能修改。
自C++11开始,引入了move语义,编译器会将这部分优化成move操作,即不再是之前的复制操作,而是move。此时,str会被进行隐式右值转换,等价于static_cast<std::string&&>(str)
,进而此处的 s
会将 func 局部返回的值进行移动。
无论是C++11之前的拷贝,还是C++11的move,str在填充(拷贝或者move)给s之后,将被销毁,而被销毁的这个值,就成为将亡值。
将亡值就定义了这样一种行为:具名的临时值、同时又能够被move。
混合类型
泛左值(glvalue, generalized lvalue),又称为广义左值,是具名表达式,对应了一块内存。glvalue有lvalue和xvalue两种形式。
一个表达式是具名的,则称为glvalue,例子如下
struct S{
int n;
};
S fun();
S s;
s;
std::move(s);
fun();
S{};
S{} n;
-
定义了结构体S和函数fun()
-
第6行声明了类型为S的变量s,因为其是具名的,所以是glvalue
-
第七行同上,因为s具名,所以为glvalue
-
第8行中调用了move函数 ,将左值s转换成xvalue,所以是glvaue
-
第10行中,fun()是不具名的,是纯右值,所以不是glvalue
-
第11行中,生成一个不具名的临时变量,是纯右值,所以不是glvalue
-
第12行中,n具名,所以是glvalue
glvalue的特征如下:
-
可以自动转换成prvalue
-
可以是多态的
-
可以是不完整类型,如前置声明但未定义的类类型
右值
右值(rvalue, right value)是指可以移动的表达式。prvalue和xvalue都是rvalue,具体的示例见下文。
rvalue具有以下特征:
-
无法对rvalue进行取地址操作。例如:
&1
,&(a + b)
,这些表达式没有意义,也编译不过。 -
rvalue不能放在赋值或者组合赋值符号的左边,例如:
3 = 5
,3 += 5
,这些表达式没有意义,也编译不过。 -
rvalue可以用来初始化const左值引用(见下文)。例如:
const int& a = 1
。 -
rvalue可以用来初始化右值引用(见下文)。
-
rvalue可以影响函数重载:当被用作函数实参且该函数有两种重载可用,其中之一接受右值引用的形参而另一个接受 const 的左值引用的形参时,右值将被绑定到右值引用的重载之上。
引用
在C++11之前,引用分为左值引用和常量左值引用两种,但是自C++11起,引入了右值引用,也就是说,在C++11中,包含如下3中引用:
-
左值引用
-
常量左值引用(不希望被修改)
-
右值引用
std::string str = "abc";
std::string &s = str;
const int &a = 10;
int &b = 10; // 错
在C++11中引入了右值引用,因为右值的生命周期很短,右值引用的引入,使得可以延长右值的生命周期。在C++中规定,右值引用是&&即由2个&表示,而左值引用是一个&表示。右值引用的作用是为了绑定右值
。
为了能区分左值引用和右值引用,代码如下:
int a = 1;
int &rb = a; // b为左值引用
int &&rrb = a; // 错误,a是左值,右值引用不能绑定左值
int &&rrb1 = 1; // 正确,1为右值
int &rb1 = i * 2; // 错误,i * 2是右值,而rb1位左值引用
int &&rrb2 = i * 2; // 正确
const int &c = 1; // 正确
const int &c1 = i * 2; // 正确
可能有人会问,除了自己根据规则区分左值引用和右值引用,有没有更快更准确的方式来判断呢?其实,系统提供了API,如下:
std::is_lvalue_reference
is_rvalue_reference
int a = 1;
int &ra = a;
int &&b = 1;
std::cout << std::is_lvalue_reference<decltype(ra)>::value << std::endl;
std::cout << std::is_rvalue_reference<decltype(ra)>::value << std::endl;
std::cout << std::is_rvalue_reference<decltype(b)>::value << std::endl;