诸如lvalue和rvalue之类的表达式类别与C ++语言的基本理论概念有关,而与其使用的实际方面无关。 因此,许多甚至经验丰富的程序员对其含义也含糊不清。 在本文中,我将尝试尽可能简单地解释这些术语的含义,并通过实际示例稀释理论。 我将立即进行预订:本文并不假装对表达式类别提供最完整,最严格的描述;有关详细信息,建议直接与C ++语言标准联系。
这篇文章将包含许多英语术语,这是由于其中一些术语很难翻译成俄文,而另一些则以不同的方式在不同的来源中进行了翻译。 因此,我经常会用斜体突出显示英语术语。
一点历史
术语左值和右值出现在C中。值得注意的是,混淆最初是在术语中造成的,因为它们是指表达式,而不是值。 从历史上看, 左值是赋值运算符可以保留的值 ,而右值是只能为right的值 。
lvalue = rvalue;
但是,这样的定义在某种程度上简化和扭曲了本质。 C89标准将左值定义为对象定位符 ,即 具有可识别存储位置的对象。 因此,不符合该定义的所有内容都包含在右值类别中。
Bjarn急救
在C ++中,表达类别的术语发展得非常强劲,尤其是在采用C ++ 11标准之后,该标准引入了右值链接和移动语义的概念。 Straustrup的文章“新”价值术语中有趣地描述了新术语出现的历史。
新的更严格的术语基于2个属性:
- 身份( identity )的存在-即一些参数,通过该参数可以理解两个表达式是否引用同一实体(例如,内存中的地址);
- 移动的能力( 可以从中移动 )-支持移动的语义。
表示身份的表达式被概括为术语glvalue ( 广义值 ),漫游表达式被称为rvalue 。 这两个属性的组合确定了3个主要的表达式类别:
实际上,C ++ 17标准引入了复制省略的概念—形式化了编译器可以并且应该避免复制和移动对象的情况。 在这方面, prvalue可能不一定要移动。 详细信息和示例可以在这里找到。 但是,这不会影响对表达类别的一般方案的理解。
在现代的C ++标准中,类别结构以这种方案的形式表示:

让我们大致检查类别的属性,以及每个类别中包含的语言表达。 我马上注意到,下面每个类别的表达式列表不能被认为是完整的;有关更准确和详细的信息,请直接参考C ++标准。
总值
glvalue类别中的表达式具有以下属性:
- 可以隐式转换为prvalue ;
- 可以是多态的,也就是说,对于他们来说,静态和动态类型的概念是有意义的;
- 不能为void类型-这直接取决于具有身份的属性,因为对于void类型的表达式,没有这样的参数可以将它们彼此区分开;
- 可以具有不完整的类型 ,例如,以向前声明的形式(如果允许特定的表达式)。
右值
rvalue类别中的表达式具有以下属性:
- 您无法在内存中获取右值地址-这直接是由于缺乏身份属性;
- 不能在赋值或复合赋值语句的左侧;
- 可用于初始化常量左值链接或右值链接,而对象的生存期延长至链接的生存期;
- 如果在调用具有2个重载版本的函数时用作参数:一个接受常数的左值引用,另一个接受右值引用,则选择接受右值引用的版本。 此属性用于实现move语义 :
class A { public: A() = default; A(const A&) { std::cout << "A::A(const A&)\n"; } A(A&&) { std::cout << "A::A(A&&)\n"; } }; ......... A a; A b(a); // A(const A&) A c(std::move(a)); // A(A&&)
从技术上讲,A &&是右值 ,可用于初始化常量左值引用和右值引用。 但是,由于有了此属性,所以没有歧义;可以接受接受右值引用的构造函数选项。
左值
特性:
- 所有glvalue属性(请参见上文);
- 您可以获取地址(使用内置的一元运算符
&
); - 可修改的左值可以在赋值运算符或复合赋值运算符的左侧;
- 可用于初始化对左值的引用(常数和非常数)。
以下表达式属于左值类别:
- 任何类型的变量,函数或类字段的名称。 即使变量是右值引用,表达式中此变量的名称也是左值 ;
void func() {} ......... auto* func_ptr = &func; // : auto& func_ref = func; // : int&& rrn = int(123); auto* pn = &rrn; // : auto& rn = rrn; // : lvalue-
- 调用返回左值引用的函数或重载运算符,或转换为左值引用类型的表达式;
- 内置赋值运算符,复合赋值运算符(
=
, +=
, /=
等),内置预增和预增( ++a
,-- --b
),内置指针解除引用运算符( *p
); - 当操作数之一是左值数组时,按索引(
a[n]
或n[a]
)访问的内置运算符; - 调用一个函数或重载的语句以返回对该函数的右值引用;
- 字符串文字,例如
"Hello, world!"
。
字符串文字与C ++中所有其他文字的不同之处仅在于它是一个左值 (尽管不可变)。 例如,您可以获取其地址:
auto* p = &”Hello, world!”; // ,
前值
特性:
- 所有右值属性(见上文);
- 不能是多态的:静态和动态表达式类型始终重合;
- 不能是不完整的类型(除了void类型,这将在下面讨论);
- 不能具有抽象类型,也不能是抽象类型的元素数组。
以下表达式属于prvalue类别:
- 文字(字符串除外),例如
42
, true
或nullptr
; - 返回非引用(
str.substr(1, 2)
, str1 + str2
, it++
)或转换表达式为非引用类型(例如static_cast<double>(x)
, std::string{}
, (int)42
); - 内置后递增和后递减(
a++
, b--
),内置数学运算( a + b
, a % b
, a & b
, a << b
等),内置逻辑运算( a && b
, a || b
!a
等),比较操作( a < b
, a == b
, a >= b
等),获取地址的内置操作( &a
); - 该指针;
- 上市项目;
- 非典型模板参数(如果不是类);
- lambda表达式,例如
[](int x){ return x * x; }
[](int x){ return x * x; }
。
值
特性:
- 所有右值属性(见上文);
- 所有glvalue属性(请参见上文)。
xvalue表达式的示例:
- 调用返回右值引用的函数或内置运算符,例如std :: move(x) ;
实际上,对于调用std :: move()的结果,您无法在内存中获取地址或初始化指向该地址的链接,但与此同时,此表达式可以是多态的:
struct XA { virtual void f() { std::cout << "XA::f()\n"; } }; struct XB : public XA { virtual void f() { std::cout << "XB::f()\n"; } }; XA&& xa = XB(); auto* p = &std::move(xa); // auto& r = std::move(xa); // std::move(xa).f(); // “XB::f()”
- 当操作数之一是右值数组时,按索引(
a[n]
或n[a]
)访问的内置运算符。
一些特殊情况
逗号运算符
对于内置的逗号运算符,表达式类别始终与第二个操作数的表达式类别匹配。
int n = 0; auto* pn = &(1, n); // lvalue auto& rn = (1, n); // lvalue 1, n = 2; // lvalue auto* pt = &(1, int(123)); // , rvalue auto& rt = (1, int(123)); // , rvalue
无效表达
调用返回void的函数,将类型转换表达式返回void并引发异常被视为prvalue表达式,但是它们不能用于初始化引用或用作函数的参数。
三元比较运算符
定义表达式的类别a ? b : c
a ? b : c
情况并非无关紧要,这完全取决于第二和第三自变量的类别( b
和c
):
- 如果
b
或c
的类型为void ,则整个表达式的类别和类型对应于另一个参数的类别和类型。 如果两个参数均为void类型,则结果为pr类型的prvalue ; - 如果
b
和c
是相同类型的glvalue ,则结果是相同类型的glvalue ; - 在其他情况下,结果是prvalue。
对于三元运算符,定义了许多规则,根据这些规则可以将隐式转换应用于参数b和c,但这在本文的范围之外;如果您感兴趣,我建议参考标准的条件运算符[expr.cond]部分。
int n = 1; int v = (1 > 2) ? throw 1 : n; // lvalue, .. throw void, n ((1 < 2) ? n : v) = 2; // lvalue, , ((1 < 2) ? n : int(123)) = 2; // , .. prvalue
对类和结构的字段和方法的引用
对于形式为am
和p->m
表达式(在这里我们讨论的是内置运算符->
),适用以下规则:
- 如果
m
是枚举元素或非静态类方法,则将整个表达式视为prvalue (尽管无法使用这样的表达式初始化链接); - 如果
a
是一个右值,而m
是一个非引用类型的非静态字段,则整个表达式属于xvalue类别; - 否则为左值 。
对于指向类成员的指针( a.*mp
和p->*mp
),规则相似:
- 如果
mp
是指向类方法的指针,则将整个表达式视为prvalue ; - 如果
a
是一个右值 ,而mp
是一个指向数据字段的指针,则整个表达式都指向xvalue ; - 否则为左值 。
位域
位字段是用于低级编程的便捷工具,但是,它们的实现在某种程度上超出了表达类别的一般结构。 例如,对位字段的调用似乎是一个左值 , 因为它可能出现在赋值运算符的左侧。 同时,通过位字段获取地址或初始化非恒定链接将无效。 您可以初始化对位字段的常量引用,但是将创建对象的临时副本:
位域[class.bit]
如果用于类型const T&的引用的初始化程序是引用位字段的左值,则该引用将绑定到临时的初始化对象,以保留位字段的值; 参考没有直接绑定到位域。
struct BF { int f:3; }; BF b; bf = 1; // OK auto* pb = &b.f; // auto& rb = bf; //
而不是结论
正如我在引言中提到的,上面的描述并不要求是完整的,而只是给出了表达类别的一般概念。 该视图将使您对标准的段落和编译器错误消息有更好的理解。