写在前面

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++中的左值、纯右值、将亡值的更多相关文章

  1. SQL中的左连接与右连接,内连接有什么不同

    SQL中的左连接与右连接,内连接有什么不同 我们来举个例子.天庭上面有一个管理系统:管理系统有个主表:主表记录着各个神仙的基本信息(我们把它当成表A).还有个表记录着他们这个神仙的详细信息(我们把它当 ...

  2. SQL中的左连接与右连接有什么区别,点解返回值会不同?(转)

    例子,相信你一看就明白,不需要多说 A表(a1,b1,c1) B表(a2,b2) a1 b1 c1 a2 b2 01 数学 95 01 张三 02 语文 90 02 李四 03 英语 80 04 王五 ...

  3. sql server中的左连接与右连接的简便写法

    左连接 *=(左表中的数据全部显示出来,右表中没有相关联的数据显示null) select Users.*,Department.name as DepartmentName from Users,D ...

  4. UICollectionView中Cell左对齐 居中 右对齐 等间距------你想要的,这里都有

    支持靠左,居中,靠右,等间距对齐. 靠左等间距.png 居中等间距.png 靠右等间距.png #import <UIKit/UIKit.h> typedef NS_ENUM(NSInte ...

  5. Linq中的左连,右连,内连

    1.左连接: var LeftJoin = from emp in ListOfEmployeesjoin dept in ListOfDepartmenton emp.DeptID equals d ...

  6. 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bccced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中

    // test20.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> #include< ...

  7. c++ 11 移动语义、std::move 左值、右值、将亡值、纯右值、右值引用

    为什么要用移动语义 先看看下面的代码 // rvalue_reference.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #includ ...

  8. 【原创】C++11:左值和右值(深度分析)

    ——原创,引用请附带博客地址 2019-12-06 23:42:18 这篇文章分析的还是不行,先暂时放在这以后再更新. 本篇比较长,需要耐心阅读 以一个实际问题开始分析 class Sub{} Sub ...

  9. Linq 和 SQL的左连接、右连接、内链接

    在我们工作中表连接是很常用的,但常用的有这三种连接方式:左连接.右连接.内链接 在本章节中讲的是1.如何在Linq中使用左连接,右连接,内连接. 2.三种连接之间的特点在哪? 3.Linq的三种连接语 ...

随机推荐

  1. [原创]LoadRunner 12.02 录制脚本时提示无Internet访问,如何解决?

    在使用LoadRunner 12.02 进行录制脚本时提示无Internet访问,如下图: 翻译中文如下: 可以尝试以下方式解决:点击弹出框中的“Yes”即可. 若还是有问题,尝试以下方式: (1)L ...

  2. visio取消自动粘附

    有时候画直线的时候需要直线摆在任意位置,这个时候自动粘附就很碍事了,总是自动把你的直线给摆到粘附的特殊位置上 如何取消: 视图->视觉帮助(点右下角的小箭头)->当前活动的->取消勾 ...

  3. Hosts文件

    Hosts是一个没有扩展名的系统文件,可以用记事本等工具打开, 其作用:就是将一些常用的网址域名与其对应的IP地址建立一个关联"数据库",当用户在浏览器中输入一个需要登录的网址时, ...

  4. Redis与Memcache的区别

    Redis与Memcache的区别 数据类型:  redis数据类型丰富,支持set liset等类型          memcache支持简单数据类型,需要客户端自己处理复杂对象 持久性: red ...

  5. Android动画之淡入淡出

    为了更好的说明Android动画的淡入淡出效果,这里以一个场景为例: 界面上有两个View 控件,两个View交替显示,当一个View淡入显示,另一个View淡出不可见. 我们把当前要显示的View叫 ...

  6. h5动画效果总结

    一些常用的h5效果,自己总结的,用的时候直接拿,方便快捷! 1.悬浮时放大: .one{transition:All 0.4s ease-in-out;-webkit-transition:All 0 ...

  7. BUG等级和严重等级关系

  8. 【金】nginx+uwsgi+django+python 应用架构部署

    网上有很多这种配置,但就是没一个靠普的,费了好大的力气才完成架构部署.顺便记录一下. 一.部署前的说明 先安装好 python,django,uwsgi,nginx软件后.后配置运行的软件是分先后的. ...

  9. [转]在 Web 项目中应用 Apache Shiro

    目录[-] 用户权限模型 图 1. 用户权限模型 认证与授权 Shiro 认证与授权处理过程 Shiro Realm 清单 1. 实现自己的 JDBC Realm 为何对 Shiro 情有独钟 与 S ...

  10. Selenium 2.0 + Java 入门之环境搭建

    最近在研究Java+Selenium的自动化测试,网上的资料比较多,自己测试实践后,整理出来一套相对比较完善的环境资料,因为网上很多下载实践的过程中,发现出现了很多不匹配的问题,什么jdk和eclip ...