话说C++中的左值、纯右值、将亡值
C++中有“左值”、“右值”的概念,C++11以后,又有了“左值”、“纯右值”、“将亡值”的概念。关于这些概念,许多资料上都有介绍,本文在拾人牙慧的基础上又加入了一些自己的一些理解,同时提出了一些需要读者特别注意的地方,主要目的有二:
1.尽可能地将这些概念介绍清楚。
2.为后续介绍完美转发和移动语义做好铺垫。
正文
一、表达式
要说清“三值”,首先要说清表达式。
定义
由运算符(operator)和运算对象(operand)①构成的计算式(类似于数学上的算术表达式)。
举例
字面值(literal)和变量(variable)是最简单的表达式,函数的返回值也被认为是表达式。
二、值类别
表达式是可求值的,对表达式求值将得到一个结果(result)。这个结果有两个属性:类型和值类别(value categories)。下面我们将详细讨论表达式的值类别②。
在c++11以后,表达式按值类别分,必然属于以下三者之一:左值(left value,lvalue),将亡值(expiring value,xvalue),纯右值(pure rvalue,pralue)。其中,左值和将亡值合称泛左值(generalized lvalue,glvalue),纯右值和将亡值合称右值(right value,rvalue)。见下图
有一点需要说明,严格来讲,“左值”是表达式的结果的一种属性,但更为普遍地,我们通常用“左值”来指代左值表达式(正如上边一段中做的那样)。所谓左值表达式,就是指求值结果的值类别为左值的表达式。通常我们无需区分“左值”指的是前者还是后者,因为它们表达的是同一个意思,不会引起歧义。在后文中,我们依然用左值指代左值表达式。对于纯右值和将亡值,亦然。
三、详细说明
事实上,无论是左值、将亡值还是纯右值,我们目前都没有一个精准的定义。它们事实上表征了表达式的属性,而这种属性的区别主要体现在使用上,如能否做运算符的左操作数、能否使用移动语义(关于移动语义,在下的后续文章中会详细介绍)等。因此,从实际应用出发,我们首先需要做到的是:给定一个表达式,能够正确地判断出它的值类别。为了使读者能够做到这一点,在下采取了一个实际的方式:先对各个值类别的特征加以描述,然后指出常见的表达式里边,哪些属于该类别。
左值
描述
能够用&取地址的表达式是左值表达式。
举例
函数名和变量名(实际上是函数指针③和具名变量,具名变量如std::cin、std::endl等)、返回左值引用的函数调用、前置自增/自减运算符连接的表达式++i/--i、由赋值运算符或复合赋值运算符连接的表达式(a=b、a+=b、a%=b)、解引用表达式*p、字符串字面值"abc"(关于这一点,后面会详细说明)等。
纯右值
描述
满足下列条件之一:
1)本身就是赤裸裸的、纯粹的字面值,如3、false;
2)求值结果相当于字面值或是一个不具名的临时对象。
举例
除字符串字面值以外的字面值、返回非引用类型的函数调用、后置自增/自减运算符连接的表达式i++/i--、算术表达式(a+b、a&b、a<<b)、逻辑表达式(a&&b、a||b、~a)、比较表达式(a==b、a>=b、a<b)、取地址表达式(&a)等。
下面从上面的例子中选取若干典型详细说明左值和纯右值的判断。
1)++i是左值,i++是右值。
前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的。假设自增前i的值是6,那么,++i得到的结果是7,这个7有个名字,就是i;而i++得到的结果是6,这个6是i加1前的一个副本,它没有名字,i不是它的名字,i的值此时也是7。可见,++i和i++都达到了使i加1的目的,但两个表达式的结果不同。
2)解引用表达式*p是左值,取地址表达式&a是纯右值。
&(*p)一定是正确的,因为*p得到的是p指向的实体,&(*p)得到的就是这一实体的地址,正是p的值。由于&(*p)的正确,所以*p是左值。而对&a而言,得到的是a的地址,相当于unsigned int型的字面值,所以是纯右值。
3)a+b、a&&b、a==b都是纯右值
a+b得到的是不具名的临时对象,而a&&b和a==b的结果非true即false,相当于字面值。
将亡值
描述
在C++11之前的右值和C++11中的纯右值是等价的。C++11中的将亡值是随着右值引用④的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关。所谓的将亡值表达式,就是下列表达式:
1)返回右值引用的函数的调用表达式
2)转换为右值引用的转换函数的调用表达式
读者会问:这与“将亡”有什么关系?
在C++11中,我们用左值去初始化一个对象或为一个已有对象赋值时,会调用拷贝构造函数或拷贝赋值运算符来拷贝资源(所谓资源,就是指new出来的东西),而当我们用一个右值(包括纯右值和将亡值)来初始化或赋值时,会调用移动构造函数或移动赋值运算符⑤来移动资源,从而避免拷贝,提高效率(关于这些知识,在后续文章讲移动语义时,会详细介绍)。当该右值完成初始化或赋值的任务时,它的资源已经移动给了被初始化者或被赋值者,同时该右值也将会马上被销毁(析构)。也就是说,当一个右值准备完成初始化或赋值任务时,它已经“将亡”了。而上面1)和2)两种表达式的结果都是不具名的右值引用,它们属于右值(关于“不具名的右值引用是右值”这一点,后面还会详细解释)。又因为
1)这种右值是与C++11新生事物——“右值引用”相关的“新右值”
2)这种右值常用来完成移动构造或移动赋值的特殊任务,扮演着“将亡”的角色
所以C++11给这类右值起了一个新的名字——将亡值。
举例
std::move()、tsatic_cast<X&&>(x)(X是自定义的类,x是类对象,这两个函数常用来将左值强制转换成右值,从而使拷贝变成移动,提高效率,关于这些,后续文章中会详细介绍。)
附注
事实上,将亡值不过是C++11提出的一块晦涩的语法糖。它与纯右值在功能上及其相似,如都不能做操作符的左操作数,都可以使用移动构造函数和移动赋值运算符。当一个纯右值来完成移动构造或移动赋值任务⑥时,其实它也具有“将亡”的特点。一般我们不必刻意区分一个右值到底是纯右值还是将亡值。
关于“三值”的大体介绍,就到此结束了。想要获知更加详细的内容,读者可以参考cppreference上的文章:
http://naipc.uchicago.edu/2015/ref/cppreference/en/cpp/language/value_category.html (精简版)
和
http://en.cppreference.com/w/cpp/language/value_category (详细版)
文章对“三值”进行了详细地讲述,同时讲出了将左值和将亡值合称泛左值的原因(这是本文未详细讨论的),如两者都可以使用多态,都可以隐式转换成纯右值,都可以是不完全类型(incomplete type)等。之所以不展开叙述,是因为在下实在举不出合适的代码来加以佐证。这里在下恳请各位读者不吝赐教。另外,关于文章(特别是详细版)中的一些观点,在下不敢苟同,篇幅原因,在下就不一一叙述了。
四、特别注意
最后,关于“三值”,有些地方需要大家特别注意。
1)字符串字面值是左值。
不是所有的字面值都是纯右值,字符串字面值是唯一例外。
早期C++将字符串字面值实现为char型数组,实实在在地为每个字符都分配了空间并且允许程序员对其进行操作,所以类似
cout<<&("abc")<<endl;
char *p_char="abc";//注意不是char *p_char=&("abc");
这样的代码都是可以编译通过的。
注意上面代码中的注释,"abc"可以直接初始化指针p_char,p_char的值为字符串"abc"的首字符a的地址。而&("abc")被编译器编译为const的指向数组的指针const char (*) [4](之所以是4,是因为编译器会在"abc"后自动加上一个'\0'),它不能初始化char *类型,即使是const char *也不行。另外,对于char *p_char="abc";,在GCC编译器上,GCC4.9(C++14)及以前的版本会给出警告,在GCC5.3(C++14)及以后的版本则直接报错:ISO C++ forbids converting a string constant to 'char*'(ISO C++禁止string常量向char*转换)。但这并不影响“字符串字面值是左值”这一结论的正确性,因为cout<<&("abc")<<endl;一句在各个版本的编译器上都能编译通过,没有警告和错误。
2)具名的右值引用是左值,不具名的右值引用是右值。
见下例(例一)
void foo(X&& x)
{
X anotherX = x;
//后面还可以访问x
}
上面X是自定义类,并且,其有一个指针成员p指向了在堆中分配的内存;参数x是X的右值引用。如果将x视为右值,那么,X anotherX=x;一句将调用X类的移动构造函数,而我们知道,这个移动构造函数的主要工作就是将x的p指针的值赋给anotherX的p指针,然后将x的p指针置为nullptr。(后续文章讲移动构造函数时会详细说明)。而在后面,我们还可以访问x,也就是可以访问x.p,而此时x.p已经变成了nullptr,这就可能发生意想不到的错误。
又如下例(例二)
X& foo(X&& x)
{
//对x进行一些操作
return x;
} //调用
foo(get_a_X());//get_a_X()是返回类X的右值引用的函数
上例中,foo的调用以右值(确切说是将亡值)get_a_X()为实参,调用类X的移动构造函数构造出形参x,然后在函数体内对x进行一些操作,最后return X,这样的代码很常见,也很符合我们的编写思路。注意foo函数的返回类型定义为X的引用,如果x为右值,那么,一个右值是不能绑定到左值引用上去的。
为避免这种情况的出现,C++规定:具名的右值引用是左值。这样一来,例一中X anotherX = x;一句将调用X的拷贝构造函数,执行后x不发生变化,继续访问x不会出问题;例二中,return x也将得到允许。
例二中,get_a_X返回一个不具名右值引用,这个不具名右值引用的唯一作用就是初始化形参x,在后面的代码中,我们不会也无法访问这个不具名的右值引用。C++将其归为右值,是合理的,一方面,可以使用移动构造函数,提高效率;另一方面,这样做不会出问题。
至此,关于“三值”的内容就全部介绍完了。
注释:
①只有当存在两个或两个以上的运算对象时才需要运算符连接,单独的运算对象也可以是表达式,例如上面提到的字面值和变量。
②确切说,是表达式的结果的值类别,但我们一般不刻意区分表达式和表达式的求值结果,所以这里称“表达式的值类别”。
③当我们将函数名作为一个值来使用时,该函数名自动转换为指向对应函数的指针。
④关于右值引用本身,没什么可说的,就是指可以绑定到右值上的引用,用"&&"表示,如int &&rra=6;。相比之下,与右值引用相关的一些主题,如移动语义、引用叠加、完美转发等,更值得我们深入探讨。这些内容,在下在后续文章中都会详细介绍。
⑤前提是该右值(如自定义的类X)有移动构造函数或移动赋值运算符可供调用(有时候是没有的,关于这些知识,后续文章在讲移动构造函数和移动赋值运算符时会详述)。
⑥在本文的例二中,如果将get_a_X()的返回值由X的右值引用改为X对象,则get_a_X()是纯右值表达式(如前所述,返回非引用类型的函数调用是纯右值),此时Foo(get_a_X());一句调用的仍然是类X的移动构造函数,这就是一个纯右值完成移动构造的例子。
写在后面
在下在参阅许多资料之后,再结合自己的理解,整理出了这篇文章,力图能实现在下写博客(不光是这篇,是所有)的初衷——为初学者服务,尽量把话说明白。但是,由于“三值”问题本身较为复杂,再加上在下才疏学浅,表达能力有限,错误疏漏及其它不足之处在所难免。所以,希望广大读者能够用批判的眼光来阅读这篇文章,更恳请大家对在下的错误疏漏提出批评指正。您的批评指正,既是对在下莫大的帮助,更是在下进步的力量源泉。
话说C++中的左值、纯右值、将亡值的更多相关文章
- SQL中的左连接与右连接,内连接有什么不同
SQL中的左连接与右连接,内连接有什么不同 我们来举个例子.天庭上面有一个管理系统:管理系统有个主表:主表记录着各个神仙的基本信息(我们把它当成表A).还有个表记录着他们这个神仙的详细信息(我们把它当 ...
- SQL中的左连接与右连接有什么区别,点解返回值会不同?(转)
例子,相信你一看就明白,不需要多说 A表(a1,b1,c1) B表(a2,b2) a1 b1 c1 a2 b2 01 数学 95 01 张三 02 语文 90 02 李四 03 英语 80 04 王五 ...
- sql server中的左连接与右连接的简便写法
左连接 *=(左表中的数据全部显示出来,右表中没有相关联的数据显示null) select Users.*,Department.name as DepartmentName from Users,D ...
- UICollectionView中Cell左对齐 居中 右对齐 等间距------你想要的,这里都有
支持靠左,居中,靠右,等间距对齐. 靠左等间距.png 居中等间距.png 靠右等间距.png #import <UIKit/UIKit.h> typedef NS_ENUM(NSInte ...
- Linq中的左连,右连,内连
1.左连接: var LeftJoin = from emp in ListOfEmployeesjoin dept in ListOfDepartmenton emp.DeptID equals d ...
- 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bccced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中
// test20.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> #include< ...
- c++ 11 移动语义、std::move 左值、右值、将亡值、纯右值、右值引用
为什么要用移动语义 先看看下面的代码 // rvalue_reference.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #includ ...
- 【原创】C++11:左值和右值(深度分析)
——原创,引用请附带博客地址 2019-12-06 23:42:18 这篇文章分析的还是不行,先暂时放在这以后再更新. 本篇比较长,需要耐心阅读 以一个实际问题开始分析 class Sub{} Sub ...
- Linq 和 SQL的左连接、右连接、内链接
在我们工作中表连接是很常用的,但常用的有这三种连接方式:左连接.右连接.内链接 在本章节中讲的是1.如何在Linq中使用左连接,右连接,内连接. 2.三种连接之间的特点在哪? 3.Linq的三种连接语 ...
随机推荐
- [原创]LoadRunner 12.02 录制脚本时提示无Internet访问,如何解决?
在使用LoadRunner 12.02 进行录制脚本时提示无Internet访问,如下图: 翻译中文如下: 可以尝试以下方式解决:点击弹出框中的“Yes”即可. 若还是有问题,尝试以下方式: (1)L ...
- visio取消自动粘附
有时候画直线的时候需要直线摆在任意位置,这个时候自动粘附就很碍事了,总是自动把你的直线给摆到粘附的特殊位置上 如何取消: 视图->视觉帮助(点右下角的小箭头)->当前活动的->取消勾 ...
- Hosts文件
Hosts是一个没有扩展名的系统文件,可以用记事本等工具打开, 其作用:就是将一些常用的网址域名与其对应的IP地址建立一个关联"数据库",当用户在浏览器中输入一个需要登录的网址时, ...
- Redis与Memcache的区别
Redis与Memcache的区别 数据类型: redis数据类型丰富,支持set liset等类型 memcache支持简单数据类型,需要客户端自己处理复杂对象 持久性: red ...
- Android动画之淡入淡出
为了更好的说明Android动画的淡入淡出效果,这里以一个场景为例: 界面上有两个View 控件,两个View交替显示,当一个View淡入显示,另一个View淡出不可见. 我们把当前要显示的View叫 ...
- h5动画效果总结
一些常用的h5效果,自己总结的,用的时候直接拿,方便快捷! 1.悬浮时放大: .one{transition:All 0.4s ease-in-out;-webkit-transition:All 0 ...
- BUG等级和严重等级关系
- 【金】nginx+uwsgi+django+python 应用架构部署
网上有很多这种配置,但就是没一个靠普的,费了好大的力气才完成架构部署.顺便记录一下. 一.部署前的说明 先安装好 python,django,uwsgi,nginx软件后.后配置运行的软件是分先后的. ...
- [转]在 Web 项目中应用 Apache Shiro
目录[-] 用户权限模型 图 1. 用户权限模型 认证与授权 Shiro 认证与授权处理过程 Shiro Realm 清单 1. 实现自己的 JDBC Realm 为何对 Shiro 情有独钟 与 S ...
- Selenium 2.0 + Java 入门之环境搭建
最近在研究Java+Selenium的自动化测试,网上的资料比较多,自己测试实践后,整理出来一套相对比较完善的环境资料,因为网上很多下载实践的过程中,发现出现了很多不匹配的问题,什么jdk和eclip ...