当前位置: 首页 > 科技观察

【现代C++】深入理解左值和右值

时间:2023-03-13 17:55:05 科技观察

本文转载自微信公众号《高性能架构探索》,作者于乐。转载本文请联系高性能架构探索公众号。大家好,我是玉乐!作为一名C/C++开发者,在平时的项目开发过程中或多或少都听说过左值和右值的概念,甚至在编译器报错时遇到过左值和右值等;甚至使用了std::move(),但不知道是什么意思。作为一个多年的C++开发者,对于左值和右值一直没有一个系统的认识,总觉得一知半解。今天,我将借助这篇文章详细介绍这些知识点,并从代码示例的角度分析什么是左值或右值。同时也算是对自己知识点的一个总结。背景作为C++开发者,相信大家都写过如下代码:voidfun(int&x){//}intmain(){fun(10);return0;}编译时会提示如下:error:invalidinitializationofnon-constreferenceoftype'int&'fromanrvalueoftype'int'wherethervalueintheaboveerroris10,也就是说,10是右值,那么什么是右值,右值是什么意思呢?这就是这篇文章这篇文章的目的是让你彻底了解C++下的值类别有哪些,以及如何区分左值、纯右值和伪值。本文的主要内容如下图所示:历史在我们正式介绍左值和右值之前,先介绍一下它的历史。编程语言CPL首次引入了值范畴,但它的定义比较简单,即对于赋值运算符,运算符左边的是左值,运算符右边的是左值。运算符是右值。C语言遵循类似于CPL的分类法,但弱化了赋值的作用。C语言中的表达式分为左值和其他(函数和非对象值),其中左值被定义为标识一个对象的表达式。但是C语言中的左值和CPL中的左值的区别在于,在C语言中,左值是locatorvalue的缩写,所以左值对应的是一个内存地址。在C++11之前,左值遵循C语言的分类法,但与C不同的是,它把非左值表达式统称为右值,函数作为左值,添加可以绑定左值的引用,但只有const引用可以绑定右值规则。几个非左值C表达式在C++中变成左值表达式。从C++11开始,对值的类别进行了详细的分类,在原有左值的基础上增加了纯右值和灭绝值,并且以上三种类型都通过了恒等性和可移动性。,并增加了glvalue和rvalue两种组合类型。在接下来的内容中,将对这些类型进行详细的说明。表达式C/C++代码由标识符、表达式和语句以及一些必要的符号(大括号等)组成。表达式由按照语言规则排列的运算符、常量和变量组成。一个表达式可以包含一个或多个操作数和零个或多个运算符来计算一个值。每个表达式都会产生一些值,这些值将在赋值运算符的帮助下分配给变量。在C/C++中,表达式有很多种,常见的有前缀和后缀表达式、条件运算符表达式等。字面量和变量是最简单的表达式,函数的返回值也算是表达式。表达式是可评估的,评估表达式会产生具有两个属性的结果:类型。这对我们来说很常见,比如int、string、reference或者我们自定义的类。类型决定了表达式可以执行的操作。值类(在下一节中介绍)。值类别在上一节中,我们提到表达式是可以求值的,值类别是求值结果的属性之一。在C++11之前,表达式的值分为左值和右值两种类型,其中右值就是我们理解中的字面值1、true、NULL等。从C++11开始,表达式的值分为左值(lvalue,leftvalue)、过期值(xvalue,expiringvalue)、纯右值(pvalue,pureravlue)和泛左值(glvalue,generalizedlvalue)两大类混合和右值(rvalue,rightvalue)五种。这五个类别的分类基于表达式的两个特征:同一性:可以确定一个表达式是否指代与另一个表达式相同的实体,例如通过比较它们标识地址的对象或函数的(直接或间接))可移动:移动构造函数、移动赋值运算符或其他实现移动语义的函数重载都可以绑定到此表达式结合以上两个特性,重新定义了五个表达式值类别:lvalue:已命名且不可移动xvaue:已命名andcanbemovedprvalue:unnamedandcanbemovedglvalue:named,lvalue和xvalue都属于glvaluervalue:expression可以移动,prvalue和xvalue都属于rvalue从glvalue和rvalue出发,结合identity和xvalue两个特性mobility,如下图所示:上图中,I代表indentity,M代表moveable。以xvalue为例。上图中xvalue为(I&M),即命名可移动。对于identity,有的文章译为havingidentity,有的文章译为named,本文统称为named。左值左值(lvalue,leftvalue),顾名思义,就是赋值符号左边的值。准确地说,左值是在表达式(不一定是赋值表达式)结束后仍然存在的对象。将左值视为具有关联名称的内存位置,允许程序的其他部分访问它。这里我们将“名称”解释为可用于访问内存位置的任何表达式。因此,如果arr是一个数组,那么arr[1]和*(arr+1)都将被视为同一内存位置的“名称”。左值具有以下特点:其地址可以通过取址运算符获得可修改的左值可以用作内置赋值,内置赋值运算符的左操作数可用于初始化左值引用(稍后描述)那么所有的左值是什么?查了相关资料,做了一些总结,基本上涵盖了所有类型:变量名、函数名、数据成员名返回左值引用的函数调用,通过赋值运算符或复合赋值运算符连接起来表达式如(a=b,a-=b,等)解引用表达式*ptr前置自增自减表达式(++a,++b)成员访问(点)运算符结果由指针访问成员(->)运算符的结果下标的结果决定运算符([])字符串字面值("abc")为了更清楚地理解左值,我们举个例子:inta=1;//a是一个左值T&f();f();//lvalue++a;//lvalue--a;//lvalueintb=a;//a和b是左值structS*ptr=&obj;//ptr是左值arr[1]=2;//左值int*p=&a;//p是左值*p=10;//*p是左值classMyClass{};MyClassc;//c是一个左值"abc"对于一个表达式,所有能成功取到它的地址(&)的操作都是左值和纯右值。前面说了,从C++11开始,纯右值(pvalue,pureravlue)等价于前面的右值,那么什么是纯右值呢?函数返回的文字值或非引用是纯右值。以下表达式的值均为纯右值:字面值(字符串字面值除外),如1、'a'、true等。返回值为非引用函数调用或运算符重载,对于example:str.substr(1,2),str1+str2,orit++后自增自减表达式(a++,a--)算术表达式逻辑表达式比较表达式取地址表达式Lambda表达式为了加深对右值的理解,以下示例是常见的纯右值:nullptr;true;1;intfun();fun();inta=1;intb=2;a+b;a++;b--;a>b;a&&b;纯右值特性:相当于C++11之前的右值,不会多态,不会是抽象类型,或者数组不会是不完整类型。Xvalue将是xvalue(xvalue,expiringvalue),顾名思义dyingvalue是C++11加入的与右值引用相关的表达式。它通常是一个要移动的对象(用于其他目的),例如返回右值引用的函数的返回值T&&,std::move的返回值,或者转换为T&&的类型转换函数的返回值。x值可以理解为“偷”其他变量的内存空间得到的值。在保证其他变量不再使用或即将被销毁时,“窃取”方式可以避免内存空间的释放和分配,可以延长变量值的生命周期。(由右值引用更新)。xvalue只能通过两种方式获得,这两种方式都涉及将左值分配(转换)为右值引用:返回右值引用的函数的调用表达式,例如static_cast(t);expression得到一个xvalue转换为rvalue引用的调用表达式,如:std::move(t),satic_cast(t)以下代码详细分析什么是xvalue:std::stringfun(){std::字符串str;//...返回str;}std::strings=fun();在函数fun()中,str是一个局部变量,在函数结束时返回。在C++11之前,s=fun();会调用复制构造函数,复制整个str,然后销毁str。如果str特别大,会造成很多额外的开销。在这一行中,s是左值,fun()是右值(prvalue),fun()生成的返回值用作临时值。str一旦被s复制,就会被销毁,无法获取或修改。从C++11开始,引入了move语义,编译器会将这部分优化为move操作,即不再是之前的copy操作,而是move。此时str会被转化为一个隐含的右值,相当于static_cast(str),这里的s会把foo返回的值移到本地。无论C++11之前的copy还是C++11的move,str在填充(复制或移动)到s后都会被销毁,被销毁的值成为垂死值。临时值定义了这样一种行为:可以同时移动的命名临时值。混合型泛左值泛左值(glvalue,generalizedlvalue),也称为广义左值,是一个命名表达式,对应一段内存。glvalue有两种形式:lvalue和xvalue。一个表达式被命名,它被称为泛左值,例子如下:structS{intn;};Sfun();Ss;s;std::move(s);fun();S{};S{}.n;上面的代码中:定义了结构体S和函数fun()。第六行声明了S类型的变量s,因为是命名的,所以是glvalue。第七行同上,因为s被命名,所以在glvalue的第8行调用了move函数将左值s转换为xvalue,所以在glvaue的第10行,fun()是匿名的,是一个纯右值,所以它不在glvalue的第11行,它生成一个未命名的临时变量是prvalue,所以它不是glvalue。在第12行,n被命名,所以它是一个glvalue。glvalue的特点是:可以自动转为prvalue,可以是多态的,也可以是不完整的类型,比如前向声明但未定义类类型右值一个右值(右值,右值)指的是可以移动的表达式。prvalue和xvalue都是右值,具体示例见下文。右值具有以下特点:不能对右值进行寻址操作。例如:&1、&(a+b),这些表达式没有任何意义,不能被编译。右值不能放在赋值或组合赋值符号的左边,例如:3=5、3+=5,这些表达式没有意义,不能编译。右值可用于初始化const左值引用(见下文)。例如:constint&a=1。右值可用于初始化右值引用(见下文)。右值可以影响函数重载:当用作函数参数并且函数有两个可用的重载时,一个接受右值引用参数,另一个接受const左值引用参数,右值重载将绑定到右值引用。通过前面的内容,我们对左值和右值(prvaluesandperishablevalues)有了初步的了解。在这一节中,我们通过一些例子来加深对左值和右值的理解。前增量(减法)是左值,后增量(减法)是纯右值。代码如下:inti=0;++i;--i;i++;i--;在上面的代码中,我们定义了一个int类型的变量i,并初始化为0。++i的操作是将i加1,然后赋值给i,所以++i的结果被命名为,和name是i,所以++i是一个左值。对于i++,先复制i的值(这里假设复制到临时变量ii),然后对i加1,最后返回ii(实际上不存在,这里为了表述方便)。所以i++是未命名的,所以它不是glvaue,所以i++是右值,并且因为它是未命名的右值,所以i++是纯右值。类似地,--i是一个左值,i--是一个纯右值算术表达式是右值代码,如下所示:intx=0;整数y=0;x+y;x&&y;x==y;在上面的代码中,x+y得到了一个未命名的临时对象,所以x+y是一个纯右值;x&&y和x==y得到一个bool常量值,要么为true要么为false,因此它是一个prvalue。解引用是左值,地址是纯右值。代码如下:intx=0;int*y=&x;*y=1;&y;*y得到y指向的地址的实际值,所以&(*y)是合法的,所以*y是左值;&y上的操作得到一个地址,也就是一个long值,所以它是一个字面值,所以&y是一个纯右值。StringliteralsarelvaluesStringliteralsarelvalues,比较特殊。如前所述,字面值都是纯右值(字符串字面值除外)。一个很重要的原因是字符串字面量值可以获得地址。以下代码在编译器中可以正常编译运行:std::cout<<&"abc"<