最近开始看C++ Templates (第二版)的英文版,自行翻译其中一些章节,给自己将来回顾的时候看,需要配合英文版服用
附录B 值的种类
表达式是C++语言的基石,提供了表达计算的主要机制。每一个表达式都有一个类型,描述了其计算产生的值的静态类型。表达式“7”的类型是int,和表达式"5+2"一样,以及和表达式”x”一样,如果x的int类型变量的话。每一个表达式也有一个“值的种类”, 这描述了其值是如何组成的,并影响表达式的行为。
B.1 传统的左值和右值
在历史上,曾经只有左值和右值两种分类。左值是”引用存储在内存或机器寄存器中的真实值的表达式的类型“,比如表达式“x“,这里x是一个变量名。这些表达式可被修改更新其所存储的值。例如,如果x是int类型的变量,接下来的赋值将替换x的值为7:
x = 7;
术语”左值(lvalue)“源于表达式在赋值中的角色:”左(l)”代表的是“左手边”,因为(历史上的C语言)只有左值能出现在赋值运算符的左手边。反之,右值(rvalue,r代表“右手边”)只能出现在赋值表达式的右手边。
然而,当C语言在1989年标准化的时候,事情发生了变化:一个存储在内存中的类型为“int const"的值是不能出现在赋值的左手边的。
int const x; //x is a nonmodifiable lvalue
x = 7; //ERROR: modifiable lvalue required on the left
C++改变的更多:右值类可以出现在赋值运算的左手边。这种赋值实际上是对该类中适合的赋值运算符的调用,而不是”单纯的“为数值类型赋值。所以他们遵从(单独的)成员函数的调用规则。
由于这些变动,术语”左值“现在通常指的是”可以定位的值”。引用变量的表达式不是唯一的一种左值表达式,其它的左值表达式包括解引用指针操作(e.g. *p),这引用的是存储在指针指向的地址中的值,以及引用一个类对象的一个成员(e.g. p->data)。 甚至用&来声明的返回“传统”的左值引用类型的函数调用也是左值。举例:
std::vector<int> v;
v.front(); //yields an lvalue because the return type is an lvalue reference
也许你没有想到,字符串字面量也是(不可更改的)左值。
右值是没有必要存储的存粹的数学上的值(例如7或者字母'a'),它们存在的目的是为了计算并且在使用过后就无法再被引用。特别地,任何除了字符串字面量之外的其他任何字面量(e.g. 7, 'a', true, nullptr)都是右值,许多内建的数值计算结果也是右值(e.g. x + 5, x是整型),以值返回的函数调用结果也是右值。也就是说,所有临时的都是右值。(然而,这不适用于存在已命名的指向他们的引用的情况)
B.1.1 左值-右值转换
由于其短暂的性质,在“单纯的”赋值中,右值有必要被限制在右手边:赋值"7 = 8"是没有意义的,因为数学上的7不允许被重新定义。另一方面,左值没有相同的限制。如果x和y是类型相容(compatible)的变量,那么赋值"x = y"是当然可以计算的, 即使表达式x和y都是左值。
赋值“x = y”可以正常工作,是因为右手边的表达式,y,经历了隐式的“左值-右值转换”。顾名思义,左值-右值转换通过读取左值在内存或寄存器中的值来产生一个类型一样的右值。这种转换完成了两件事:第一个,它保证左值可以在任何期望使用右值的地方被使用(e.g. 在赋值运算的右手边或者在一个数值表达式中比如x + y)。 第二个是它可以识别出编译器可能会程序中的哪里会发出一条“加载(load)”的指令去从内存中读值(在优化之前)。
B.2 c++11 以来的值的种类
由于C++11 引进了右值引用来支持移动语义,传统的将表达式分类为左值和右值已经不足以描述全部的C++语言的行为了。因此C++标准委员会重新定义了值的分类系统,包括三个核心分类和两个复合分类。如下图所示。核心的分类是: lvalue, prvalue("pure rvalue"), xvalue。复合分类是: glvalue("generalized lvalue", the union of lvalue and xvalue), rvalue(the union of xvalue and prvalue)。
注意所有的表达式依旧要么是左值,要么是右值,只不过现在右值被进一步细分了。
这种C++11的分类方法至今仍然有效,只是在C++17中分类的描述被重新定义为如下:
glvalue 是其求值结果可以确定一个对象、位域(bit-filed)或函数标识的表达式
pravlue是其求值结果如何下列之一的表达式:
●计算某个运算符的操作数的值
●初始化某个对象或位域(bit-field)
xvalue是代表其资源能够被重新使用的对象或位域的泛左值(通常因为这是将要"死亡"的,the "x" in xvalue 来自于"eXpiring value")
lvalue 是非xvalue的glvalue
rvalue 是prvalue或xvalue
注意,在C++17中(某种程度上,在C++11和C++14中),按照glvalue和prvalue来分类可能比传统上按照lvalue和rvalue来区分更加的根本性(more fundamental)
虽然前面讲这些描述是在C++17才引入的,不过这些描述对C++11和C++14依然适用(先前的描述是等价的,不过更难以理解)。
除去位域,glvalues会产生一个拥有地址的实例。这个地址可能是一个较大的封闭对象的子对象的地址。对于一个基类子对象来说,glvalue(表达式)的类型被叫做静态类型(static type),而"基类是其一部分的最终派生出来的对象"的类型被叫做glvalue的动态类型(dynamic type)。如果一个glvalue不产生基类子对象,则其静态类型和动态类型是相同的(i.e.,就是表达式的类型)。
下面是lvalue的例子:
●指定变量、函数的表达式
●使用内建的单目运算符"*"(间接寻址)
●字符串字面量
●函数调用,其返回类型是左值引用
下面是prvalue的例子:
●除了字符串字面量和user-defined字面量之外的字面量(注:User-defined 字面量会导致lvalues或rvalues,取决于相关的字面量运算符返回类型)
●使用内建的单目运算符"&"(获取表达式的地址)
●使用内建的算术运算符
●函数调用,其返回类型是非引用
●lambda表达式
下面是xvalue的例子:
●函数调用,其返回类型为对象的右值引用(e.g. std::move())
●转换为对象的右值引用类型的转型表达式(e.g. static_cast<char&&>())
注意,函数类型的右值引用是lvalue,而不是xvalue (e.g. 返回类型是到函数的右值引用的函数调用或者转换为函数的右值引用类型的转型表达式 static_cast<void (&&)(int)>())
值得强调的是,glvalues, prvalues, xvalues and so on 这些是表达式,而不是值或实例(注:很不幸这意味着这些术语是用词不当的)。例如,一个变量不是一个lvalue,尽管表示变量的表达式是lvalue
int x = 3; //这里x是一个变量,不是一个lvalue,3 是一个用来初始化变量x的prvalue
int y = x; //这里x是一个lvalue。 这个lvalue表达式的求值结果不会产生值3,而是指定一个持有值3的对象。这个lvalue接下来会转换为一个prvalue,用来初始化y
B.2.1 临时量实质化( Temporary Materialization)
我们之前提到过lvalue经常会经历lvalue-to-rvalue转换(在C++11的值分类中,使用glvalue-to-prvalue转换更为准确,不过传统的术语更为通用),因为初始化对象(或者为大多数内建操作符提供操作数)需要prvalue类型的表达式。
在C++17中,存在这种转换的反向转换,称为"临时量实质化(temporary materialization)"(也可以叫做"prvalue-to-xvalue"转换): 任何时候一个prvalue合法地出现在一个期待glvalue(包含xvalue情况)的地方时,就会产生一个临时对象并用该prvalue来初始化(回忆一下,prvalues主要是"初始化值"),这个prvalue就会被指定这个临时对象的xvalue取代。举例:
int f(int const&);
int r = f(3);
在本例中,因为f()有一个引用形参,所以它期望一个glvalue的实参。然而表达式3是一个prvalue。因此"临时量实质化"规则开始生效,表达式3被转换为一个指定"用值3初始化的临时对象"的xvalue.
更普遍地,在下列情形中,一个临时值会被实质化,并用一个prvalue来初始化:
●绑定引用到prvalue时(e.g. 上面调用f(3))
●在类pravlue上进行成员访问时
●在数组prvalue上使用下标时
●进行数组prvalue到指针的转换时(i.e. array decay)
●以花括号初始化器列表初始化std::initializer_list<T>类型的对象时
●对prvalue使用typeid 或 sizeof运算符时
●prvalue作为形式为"expr;"的顶层表达式的声明时,或者作为转型到类型void的转型表达式的实参。
因此,在C++17中,什么时候用一个prvalue来初始化对象总是由上下文来决定的,这也导致只有在临时量真正被需要的时候才会被创建出来。在C++17之前,prvalues(特别是类类型)总是意味着一个临时量。对于这些临时量的拷贝也许会被可选择的省略掉,但是编译器仍旧强制保证拷贝操作的语义约束(e.g.拷贝构造函数是可被调用的)。下面这个例子展现了这条规则在C++17中的结果
class N{
public:
N() = default;
N(const N&) = delete; //this class is neither copyable nor movable
N(N&&) = delete;
}
N make_N(){
return N{}; //Always creates a conceptual temporary prior to C++17. In C++17, no temporary is created at this oint.
}
auto n = make_N(); //Error prior to C++17 because the prvalue needs a conceptual copy. Ok since C++17, because n is initialized directly from the prvalue;
在C++17之前,N{}作为prvalue会产生一个类型为N的临时量,不过编译器被允许省略其复制和移动的过程(实践中它们也经常是这么做的)。在这个例子中,这意味着通过调用make_N()产生的临时结果可以直接在n的内存中构造出来而不再需要拷贝或移动操作。不幸的是,在C++17之前,编译器依然不得不检查移动或拷贝操作是可用的,而在本例中是不可用的,因为N的拷贝构造和移动构造函数被声明为delete,因此在C++11和C++14的编译器会报错。
在C++17中,作为prvalue的N自身不会产生一个临时量,相反她会根据上下文而去初始化一个对象:在本例中,这个对象由n表示。不用再去考虑拷贝或者移动操作(这是语言特性保证的,而不是一项优化),因此,在C++17中代码是合法的。
我们通过展现多种值分类的情形的例子来结束本节:
class X{};
X v;
const X c;
void f(const X&); //accepts an expression of any value category
void f(X&&); //accepts prvalues and xvalues only but is a better much for those than the previous declaration
f(v); //passes a modifiable lvalue to the first f()
f(c); //passes a nonmodifiable lvalue to the first f()
f(X()); //passes a prvalue(since C++17 materialized as xvalue) to the 2nd f()
f(std::move(v)); //passes an xvalue to the second f()
B.3 使用decltype来检查值的分类
通过使用关键字decltype(自C++11中引入),使检查任意的C++表达式的值分类成为可能。对于类型为T的任意的表达式x,decltype((x))(注意,两层括号)代表
T 如果x是prvalue
T& 如果x是lvalue
T&& 如果x是xvalue
两层括号是用来在当表达式x只是一个变量名的时候,decltype((x))需要避免产生变量x的声明类型(其他情况下,括号不起作用)。举例,如果表达式x就是变量名v,没有括号的结构成了decltype(v),这会生成变量v的类型,而不是一个“反映了表达式x所引用变量的值类别的”类型
因此,对任意表达式e使用类型特性(type trais),我们可以检查其值的分类:
更多详情,请见298页15.10.2章节
B.4 引用类型
C++中的引用类型,就像int&,会通过两种途径来影响值的分类。第一种,一个引用会限制其可以绑定的表达式的值的类别。举例,一个非const的lvalue引用类型int&只可以被int类型的lvalue表达式初始化。类似地,一个rvalue引用int&&只可以被int类型的rvalue表达式初始化。
引用影响值的分类的第二种方式是通过函数的返回值,也就是说使用引用类型作为返回类型会影响调用该函数的表达式的值的分类。特别是:
●调用返回类型是lvalue reference的函数的表达式代表一个lvalue
●调用返回类型是一个对象的rvalue reference的函数的表达式代表一个xvalue(注意,函数类型的rvalue reference的结果类型是lvalue)
●调用返回类型不是引用类型的函数的表达式代表一个prvalue
我们通过下面的例子来说明引用类型和值分类的关系:
int& lvalue();
int&& xvalue();
int prvalue();
值的分类和所给表达式的类型都可以通过decltype来确定。就像298页15.10.2章节所描述的,它通过引用类型来描述表达式是lvalue还是xvalue:
std::is_same_v<decltype(lvalue()), int&> //yields true because result is lvalue
std::is_same_v<decltype(xvalue()), int&&> //yields true because result is xvalue
std::is_same_v<decltype(prvalue()), int> //yields true because result is prvalue
再看下面这些调用:
int& lref1 = lvalue(); //OK: lvalue reference can bind to an lvalue
int& lref3 = prvalue(); //ERROR: lvalue reference cannot bind to a prvalue
int& lref2 = xvalue(); //ERROR: lvalue reference cannot bind to an xvalue
int && rref1 = lvalue(); //ERROR: rvalue reference cannot bind to an lvalue
int&& rref2 = prvalue(); //OK: rvalue reference can bind to a prvalue
int&& rref3 = xvalue(); //OK: rvalue reference can bind to an xvalue