lvalue-to-rvalue转换是在什么时候发生的,它是如何工作的,是否会失败?

51
我在C++标准中看到了很多地方都使用了"lvalue-to-rvalue conversion"这个术语。据我所知,这种转换通常是隐式进行的。
标准中的措辞让我感到意外的一个特点是,他们决定将lvalue-to-rvalue视为一种转换。如果他们说glvalue始终可接受而不是prvalue,那么这个措辞实际上会有不同的含义吗?例如,我们读到lvalues和xvalues是glvalues的例子,但我们没有读到lvalues和xvalues可以转换为glvalues。这是否有不同的含义?
在我第一次遇到这个术语之前,我对lvalues和rvalues的模型大致如下: lvalues总是能够作为rvalues,而且还可以出现在等号的左侧和"&"的右侧。
对我来说,这是一种直观的行为,即如果我有一个变量名,那么我可以将该名称放在任何我会放置字面值的地方。只要这种隐式转换是保证发生的,这个模型似乎与标准中使用的lvalue-to-rvalue隐式转换术语是一致的。
但是,因为他们使用了这个术语,我开始怀疑隐式的左值到右值转换在某些情况下可能会失败。也就是说,也许我的心理模型在这里是错误的。以下是相关的措辞 [basic.lval] p6(感谢评论者):
每当 glvalue 出现在需要该操作数的 prvalue 的运算符的操作数位置上时,将应用左值到右值、数组到指针或函数到指针的标准转换将表达式转换为 prvalue。
[注:试图将一个右值引用绑定到一个左值不是这样一个上下文;参见 [dcl.init.ref]。— 结束注释]
我理解说明中的注释描述的是以下内容:
int x = 1;
int && y = x; //in this declaration context, x won't bind to y.
// but the literal 1 would have bound, so this is one context where the implicit 
// lvalue to rvalue conversion did not happen.  
// The expression on right is an lvalue. if it had been a prvalue, it would have bound.
// Therefore, the lvalue to prvalue conversion did not happen (which is good). 

所以,我的问题是:
1. 有人能解释一下这种转换可以隐式发生的上下文吗?具体来说,除了绑定到右值引用的上下文之外,还有其他哪些情况下左值到右值的转换无法隐式发生?
2. 如果rvalue-reference绑定不是我们期望出现prvalue表达式(在右边)的上下文,那么是什么呢?
3. 像其他转换一样,glvalue-to-prvalue转换是否涉及运行时的工作,使我能够观察到它?
我的目标不是询问是否允许这种转换是可取的。我试图从标准开始,学会用自己的话解释这段代码的行为。
一个好的回答会仔细解读我上面引用的语句,并解释(基于对文本的分析)其中的注释是否也隐含在文本中。然后,它可能会添加其他引用,让我了解这种转换可能无法隐式发生的其他情境,或者解释没有更多类似的情境。也许还可以对为什么将glvalue转换为prvalue被认为是一种转换进行一般性的讨论。

8
注意,int && y = x; 不是一个表达式,而是一个声明。因此,“等号右侧的操作数”不会自动应用lvalue-to-rvalue转换。 - dyp
4
这不是左值到右值转换失败了,而是右值引用绑定失败了。 - Lightness Races in Orbit
1
@DyP:嗯,右边还有一个表达式……但我认为标准实际上是说,“当需要rvalue时;绑定到引用不是这种情况”。 - Kerrek SB
@DyP:我认为是3.10/2。 - Kerrek SB
2
@orm 预期为 xvalue(纯右值)。当您使用 prvalue 初始化 rvalue 引用时,它实际上会绑定到一个用该 prvalue 初始化的临时 xvalue。换句话说,int && x = 1; ++x; 实际上并不更改 1 的值。但是 int y = 42; int &&x = std::move(y); ++x; 将更改 y 的值。 - Casey
显示剩余19条评论
3个回答

48

我认为lvalue到rvalue的转换不仅仅是“在需要rvalue的地方使用lvalue”。它可以创建类的副本,并始终产生一个值,而不是对象。

我正在使用n3485进行“C ++ 11”和n1256进行“C99”。


对象和值

C99 / 3.14中最简明的描述如下:

对象

执行环境中的数据存储区域,其内容可以表示值

C++11 / [intro.object] / 1中也有一些内容:

有些对象是多态的;实现会生成与每个这样的对象相关联的信息,使得在程序执行期间可以确定该对象的类型。 对于其他对象,其中找到的值的解释由用于访问它们的表达式的类型决定。

因此,对象包含值(可以包含)。


值类别

尽管名称为值类别,但值类别分类的是表达式,而不是值。即使lvalue表达式也不能被视为值。

完整的分类可以在[basic.lval]中找到; 这里是StackOverflow的讨论

以下是关于对象的部分:

  • lvalue([...])指定函数或对象。[...]
  • xvalue(“过期”值)也引用对象[...]
  • glvalue(“广义”lvalue)是lvalue或xvalue。
  • rvalue([...])是xvalue、临时对象或其子对象,或者与对象不相关联的值。
  • prvalue(“纯”rvalue)是不是xvalue的rvalue。[...]

请注意短语“与对象无关联的值”。还要注意,由于xvalue-expression引用对象,因此真正的必须始终出现为prvalue-expression。


lvalue-to-rvalue转换

如脚注53所示,现在应将其称为“glvalue-to-prvalue转换”。首先,这里是引用:

1    非函数和非数组类型 T 的 glvalue 可以转换为 prvalue。如果 T 是不完整类型,则需要此转换的程序是不合法的。如果 glvalue 所指的对象不是 T 类型的对象,也不是派生自 T 的对象,或者对象未初始化,则需要此转换的程序具有未定义的行为。如果 T 是非类类型,则 prvalue 的类型是 T 的 cv-未限定版本。否则,prvalue 的类型是 T

这个段落规定了转换的要求和结果类型,但还没有涉及到转换的效果(除了 undefined behavior)。

2    当在未求值操作数或其子表达式中发生 lvalue 到 rvalue 转换时,不访问所引用对象中包含的值。否则,如果 glvalue 有一个类类型,则将类型为 T 的临时变量从 glvalue 复制初始化,并且转换的结果是该临时变量的 prvalue。否则,如果 glvalue 具有 (可能带有 cv 限定符的) 类型 std::nullptr_t,则 prvalue 结果是空指针常量。否则,由 glvalue 指示的对象中包含的值是 prvalue 的结果。

我认为你最常见的是将 lvalue-to-rvalue 转换应用于非类类型。例如,

struct my_class { int m; };

my_class x{42};
my_class y{0};

x = y;

表达式 x = y 不会对 y ���行左值到右值的转换(顺便提一下,那样会创建一个临时的 my_class)。原因是 x = y 被解释为 x.operator=(y),默认情况下,它以引用方式接受 y,而非值传递(关于引用绑定,请看下面的内容;它无法将右值绑定到引用,因为那会创建一个不同于 y 的临时对象)。然而,默认定义的 my_class::operator= 确实对 x.m 执行了左值到右值的转换。
因此,对我来说最重要的部分似乎是

否则,由 glvalue 指示的对象中包含的值是 prvalue 结果。

所以通常,左值到右值的转换只会从一个对象中读取值。这不仅仅是值(表达式)类别之间的空操作转换;甚至可以通过调用复制构造函数来创建一个临时对象。而左值到右值的转换始终返回 prvalue 值,而非(临时)对象。
请注意,左值到右值的转换并不是唯一将左值转换为 prvalue 的转换:还有数组到指针的转换和函数到指针的转换。

值和表达式

大多数表达式都不产生对象[[需要引证]]。然而,id-表达式可以是一个标识符,它表示一个实体。对象是实体,因此有些表达式会产生对象:
int x;
x = 5;
x = 5 的赋值表达式左侧也需要是一个表达式。这里的 x 是一个标识符,因此是一个 id-expression。该 id-expression 的结果是x所表示的对象
表达式应用隐式转换:[expr]/9

每当 glvalue 表达式作为预期 prvalue 的运算符的操作数出现时,都会应用 lvalue-to-rvalue、array-to-pointer 或 function-to-pointer 标准转换将表达式转换为 prvalue。

另外还有关于usual arithmetic conversions的/10以及用户定义转换的/3。
我很想引用一个“期望其操作数为 prvalue”的运算符,但是除了强制类型转换之外找不到任何例子。例如,[expr.dynamic.cast]/2:“如果 T 是指针类型,则 v [操作数] 必须是指向完整类类型的指针的 prvalue”。
许多算术运算符所需的 usual arithmetic conversions 间接调用 lvalue-to-rvalue 转换,通过使用所使用的标准转换。所有标准转换(除了从 lvalues 转换为 rvalues 的三个转换)都期望 prvalues。
然而,简单的赋值并不会调用 usual arithmetic conversions。它在 [expr.ass]/2 中被定义为:

在简单赋值中(=),表达式的值替换了左操作数所引用的对象的值。

因此,尽管它没有明确要求右侧为 prvalue 表达式,但它确实需要一个。我不确定这是否严格需要 lvalue-to-rvalue 转换。有一种观点认为,无论是通过将其值分配给对象还是通过将其值添加到另一个值中来访问未初始化变量的值应始终会导致未定义行为(也请参见 CWG 616)。但这种未定义行为仅对 lvalue-to-rvalue 转换(据我所知)作出了要求,然后应该是访问存储在对象中的值的唯一方式。
如果这种更概念性的观点是有效的,即我们需要 lvalue-to-rvalue 转换来访问对象内部的值,则更容易理解它在哪里(以及需要进行)。

初始化

与简单赋值一样,在初始化另一个对象时是否需要 lvalue-to-rvalue 转换存在讨论
int x = 42; // initializer is a non-string literal -> prvalue
int y = x;  // initializer is an object / lvalue

对于基本类型,[dcl.init]/17的最后一条规定如下:
否则,正在初始化的对象的初始值是初始化表达式的(可能转换过的)值。必要时将使用标准转换将初始化表达式转换为目标类型的cv-unqualified版本;不考虑用户定义的转换。如果无法进行转换,则初始化是非法的。
然而,它也提到了初始化表达式的值。类似于简单赋值表达式,我们可以将其视为对lvalue-to-rvalue转换的间接调用。
参考绑定
如果我们将lvalue-to-rvalue转换视为访问对象的值的一种方式(加上为类类型操作数创建临时对象),我们就会明白,它通常不适用于绑定到引用:引用是一个lvalue,它总是指向一个对象。因此,如果我们将值绑定到引用,我们需要创建包含这些值的临时对象。如果引用的初始化表达式是prvalue(即值或临时对象),那么这确实是情况:
int const& lr = 42; // create a temporary object, bind it to `r`
int&& rv = 42;      // same

禁止将prvalue绑定到lvalue引用上,但是具有返回lvalue引用的转换函数的类类型的prvalue可以绑定到转换类型的lvalue引用上。

[dcl.init.ref]中对于引用绑定的完整描述相当冗长且与本问题无关。我认为与本问题相关的本质是引用是指向对象的,因此不进行glvalue-to-prvalue(对象到值)转换。


10
可能需要一个 TL;DR。 - dyp
@ShafikYaghmour 通过使用一元运算符*进行间接引用可以得到一个左值,[expr.unary.op]/1 - dyp
@ShafikYaghmour 啊,你是指应用于指针的左值到右值转换(我是指间接引用产生的左值)。是的,在这里似乎没有直接规定任何左-右转换。即使是单词value在[expr.unary.op]/1中也没有出现。(这似乎也适用于指针算术,如我的评论中所述。) - dyp
1
在C++标准的术语中,运算符不是函数。dynamic_cast也不是函数,即使它看起来像一个函数。由于值类别和左值到右值转换适用于_表达式_,我们真的必须从编译器的角度来看待语言。您可以在clang的AST转储中看到这一点:https://godbolt.org/z/vGP1hfoM5,请查看ImplicitCastExpr,它是一个LValueToRvalue(转换)。 - dyp
@JMC 我认为将一个 string 变量传递给 void SetName(string name); 函数可以被视为一个左值到右值转换的示例,从而创建一个副本。 - undefined
显示剩余8条评论

5
关于 glvalues:glvalue(“广义”lvalue)是一个表达式,它可以是 lvalue 或 xvalue。
通过 lvalue-to-rvalue、array-to-pointer 或 function-to-pointer 隐式转换,glvalue 可以被隐式转换为 prvalue。
当 lvalue 参数(例如对象的引用)在期望 rvalue(例如数字)的上下文中使用时,将应用 Lvalue 转换。
Lvalue 转换
任何非函数、非数组类型 T 的 glvalue 都可以被隐式转换为相同类型的 prvalue。如果 T 是非类类型,则此转换还会删除 cv 限定符。除非在未评估的上下文中遇到(在 sizeof、typeid、noexcept 或 decltype 的操作数中),否则此转换有效地使用原始 glvalue 作为构造函数参数复制构造类型为 T 的临时对象,并将该临时对象作为 prvalue 返回。如果 glvalue 具有类型 std::nullptr_t,则生成的 prvalue 是空指针常量 nullptr。

1st. 在C++中,如何期望rvalue(例如数字)?在声明函数时,您只能指定参数类型。我认为所有函数都可以接受所有值类别的表达式(示例)。 2nd. 是否有一个例子,其中将lvalue参数传递给函数,但是lvalue不够好,因此编译器会复制构造相同类型的prvalue以将其传递给函数?听起来很奇怪。 - Dr. Gut
@Dr.Gut 如果你有 string myName;void SetName(string name);SetName(myName) 会从 myName 创建一个临时的 string 并将临时的字符串传递给 SetName - undefined
当然。但我认为他们不称之为左值到右值的转换。 - undefined

0
在深入细节之前,你应该知道“lvalue-to-rvalue conversion”意味着从内存中读取数据。
因此,在运行时它可能会失败,如果lvalue是无效的(由无效指针形成的引用,目标对象已经超出作用域的悬空引用等)或者结果是未初始化的。
现在,编译器优化可能会重新排序实际的内存读取,通过将值缓存在CPU寄存器中来合并多个读取操作等。但是其含义始终是获取对象的现有值。
即使优化器已经转换了代码以避免实际进行内存读取,如果假设的内存读取不成功,你将得到未定义的行为。可能的重排序/推测性读取是“时间旅行未定义行为”的一种来源,其中达到从无效lvalue中获取数据的代码路径会在较早时期开始表现异常。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接