C++17的值类别

几年前写的一篇C++11的值类别,有朋友指出了里面的一些问题

改完小问题以后,回头看感觉其实写的很烂,很多重要内容没有提到,当然这和C++11的值类别本身比较晦涩,我当时理解的也很烂有关系

刚好C++17简化了值类别,那么直接再写一篇关于值类别的文章

表达式的类型和值类别

表达式有两个属性

  • 类型:描述计算产生的值的静态类型
  • 值类别:描述值是如何产生的,以及表达式的行为如何被影响

表达式之间能够赋值,不只是需要判断类型,还需要判断值类别是否成立

例如表达式“7”的类型是int,表达式“5+2”的类型是int

7 = 5+2这个表达式无法编译,两个表达式的类型成立,但是值类别无法成立

传统的值类别

在=号左边的就是左值,右边的就是右值

回顾C++98的值类别

在C++11以前,值类别几乎没有什么存在感

根据C++98的标准定义(3.10 Lvalues and rvalues)

Every expression is either an lvalue or an rvalue.

An lvalue refers to an object or function. Some rvalue expressions—those of class or cv-qualified class type—also refer to objects

这个定义很模糊,左值是指对象或函数,右值也是对象,基本上等于啥也没说

随后标准列举了一些表达式是左右值细则,其中有一条是

Whenever an lvalue appears in a context where an rvalue is expected, the lvalue is converted to an rvalue;
see 4.1, 4.2, and 4.3.

然后在4.1 Lvalue-to-rvalue conversion中正式定义了左值到右值的转换:

An lvalue (3.10) of a non-function, non-array type T can be converted to an rvalue.

因此对表达式"5+2"来说,它是一个左值,但是当他出现在等号右边时,会隐式转换成右值

回顾C++11的值类别

到了C++11以后,值类别被重新进行了定义

img

  • An lvalue (so called, historically, because lvalues could appear on the left-hand side of an assignment expression) designates a function or an object. [Example: If E is an expression of pointer type, then *E is an lvalue expression referring to the object or function to which E points. As another example, the result of calling a function whose return type is an lvalue reference is an lvalue. —end example ]
  • An xvalue (an “eXpiring” value) also refers to an object, usually near the end of its lifetime (so that its resources may be moved, for example). An xvalue is the result of certain kinds of expressions involving rvalue references (8.3.2). [Example: The result of calling a function whose return type is an rvalue reference is an xvalue. —end example ]
  • A glvalue (“generalized” lvalue) is an lvalue or an xvalue.
  • An rvalue (so called, historically, because rvalues could appear on the right-hand side of an assignment expression) is an xvalue, a temporary object (12.2) or subobject thereof, or a value that is not associated with an object.
  • A prvalue (“pure” rvalue) is an rvalue that is not an xvalue. [Example: The result of calling a function whose return type is not a reference is a prvalue. The value of a literal such as 12, 7.3e5, or true is also a prvalue. —end example ]

由于被分为了左值(lvalue),将亡值(xvalue)和纯右值(pvalue)三个最细分的类型,因此直接读标准显得非常难以理解

根据cppreference的定义

With the introduction of move semantics in C++11, value categories were redefined to characterize two independent properties of expressions[5]:

  • has identity: it’s possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);
  • can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.

In C++11, expressions that:

  • have identity and cannot be moved from are called lvalue expressions;
  • have identity and can be moved from are called xvalue expressions;
  • do not have identity and can be moved from are called prvalue (“pure rvalue”) expressions;
  • do not have identity and cannot be moved from are not used[6].

可以看到,cppreference的定义分为2个类型以后简单了一些

拥有身份(glvalue) 不拥有身份
可移动(rvalue) xvalue prvalue
不可移动 lvalue 不存在

值类别被简化为了两个问题,什么是拥有身份?什么是可移动?

根据cppreference引用的文章https://www.stroustrup.com/terminology.pdf

  • “has identity” – i.e. and address, a pointer, the user can determine whether two copies are identical, etc.

  • “can be moved from” – i.e. we are allowed to leave to source of a “copy” in some indeterminate, but valid state

根据我的理解:

  • 拥有身份

    是有名字的表达式,可以确定表达式是否与另一表达式指代同一实体,可以取地址

  • 可移动

    对象的资源可以移动到别的对象中,移动是指一种使原表达式失效的“拷贝”

    也可以根据身份来解释:表达式不拥有身份,或者拥有身份且生命周期即将结束的表达式

4.1 Lvalue-to-rvalue conversion也进行了修改

A glvalue (3.10) of a non-function, non-array type T can be converted to a prvalue.

扩大了左值转换到右值的范围,使得xvalue也可以进行转换

C++17的值类别

C++11的值类别即使在简化理解以后,依然有点让人懵逼,看到一个表达式需要先思考身份和可移动,这非常晦涩,需要对值类别进行简化

另外在C++17中,为了解决NRVO能干掉move/copy,但是还是要求得有move/copy构造函数的问题,需要对值类别进行修改

因此C++17迎来了值类别的优化

首先看下为什么需要对值类别进行修改

NRVO和C++11语法上的冲突

看如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
struct NonMovable
{
NonMovable() noexcept = default;
NonMovable(NonMovable&&) noexcept = delete;
NonMovable& operator=(NonMovable&&) noexcept = delete;
};
NonMovable Make()
{
return NonMovable{};
}
int main() {
[[maybe_unused]] const auto x = Make();
}

这里编译是必然失败的,因为NRVO虽然NRVO能干掉move/copy,但是这只是编译器优化

在C++11的语法上,Make函数需要move/copy构造函数之一,而x的赋值需要move构造函数

我在工作中经常因为这个问题踩坑,由于C++11语法无法绕过,因此我只能绕过NRVO改为引用传参将结果返回,整个代码一点也不优雅,auto也用不了了

解决办法

主要需要解决什么情况下省略拷贝和移动,但是如果加入一个新的值类别来描述,那我只能说真的学不动了

由于xvalue和prvalue都是可以被移动的,因此可以修改值类别

要求只有xvalue可以被移动,prvalue不能被移动,但是可以隐式转换为xvalue

因此prvalue用于初始化的情况下,可以省略拷贝,而其他不能省略的情况下,隐式转换为xvalue进行移动

由于在C++11中,使用可移动划分了rvalue这个分类

在C++17中,这个概念被弱化了

拥有身份(glvalue) 不拥有身份
xvalue, lvalue prvalue

通过是否拥有身份划分为了glvalue和prvalue,这大大简化了理解成本

img

最后来看下C++17标准定义

  • A glvalue(generalized lvalue) is an expression whose evaluation determines the identity of an object, bit-field, or function.
  • A prvalue(pure rvalue) is an expression whose evaluation initializes an object or a bit-field, or computes the value of an operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.
  • An xvalue(eXpiring value) is a glvalue that denotes an object or bit-field whose resources can be reused(usually because it is near the end of its lifetime)。
  • An lvalue is a glvalue that is not an xvalue.
  • An rvalue is a prvalue or an xvalue.

可以简单地理解成 glvalue 表示对象或函数,prvalue 表示只有在初始化对象时才能发挥作用的值。

然后标准加入了7.4 Temporary materialization conversion

A prvalue of type T can be converted to an xvalue of type T

This conversion initializes a temporary object of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object

T shall be a complete type

1
2
struct X { int n; };
int k = X().n; // OK, X() prvalue is converted to xvalue

当期待glvalue的地方出现了prvalue,会创建一个临时对象,用prvalue初始化,得到的xvalue作为临时对象

值类别的检测

可以通过标准库来检查当前的值类别

1
2
3
4
5
6
7
if constexpr (std::is_lvalue_reference_v<decltype((e))>) {
std::cout << "expression is lvalue" << "\n";
} else if constexpr (std::is_rvalue_reference_v<decltype((e))>) {
std::cout << "expression is xvalue" << "\n";
} else {
std::cout << "expression is prvalue" << "\n";
}

这里是decltype((e))的两层括号是必须的(例如,如果表达式x只是将一个变量命名为v,那么decltype((v))的结构将变成decltype(v),它将生成变量v的类型)

引用类型的组合

在C++ Templates The Complete Guide (2nd Edition)的附录中有以下代码,觉得有点意思就post在这里

1
2
3
4
5
6
7
8
9
10
int& lvalue();
int&& xvalue();
int prvalue();

int& lref1 = lvalue(); // OK: lvalue reference can bindto 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 bindto a prvalue
int&& rref3 = xvalue(); // OK: rvalue reference can bindto an xrvalue

参考资料