48

我看到在整个 C++ 标准的许多地方都使用了术语“左值到右值转换”。据我所知,这种转换通常是隐式完成的。

标准措辞的一个出乎意料的(对我而言)特征是他们决定将左值到右值视为一种转换。如果他们说glvalue总是可以接受而不是prvalue怎么办。这句话真的会有不同的含义吗?例如,我们读到 lvalues 和 xvalues 是 glvalues 的例子。我们没有读到左值和 xvalue 可以转换为 glvalues。意思有区别吗?

在我第一次遇到这个术语之前,我曾经在心理上或多或少地对左值和右值进行建模,如下所示:“左值总是能够充当右值,但此外还可以出现在 an 的左侧和 an=的右侧&”。

对我来说,这是一种直观的行为,如果我有一个变量名,那么我可以把这个名字放在我应该放文字的任何地方。这个模型似乎与标准中使用的左值到右值隐式转换术语一致,只要保证这种隐式转换发生。

但是,因为他们使用这个术语,我开始怀疑隐式左值到右值的转换在某些情况下是否会失败。也就是说,也许我的心智模型在这里是错误的。这是标准的相关部分:(感谢评论者)。

每当一个泛右值出现在预期纯右值的上下文中时,该泛左值就被转换为一个纯右值;见 4.1、4.2 和 4.3。[注意:将右值引用绑定到左值的尝试不是这样的上下文;见 8.5.3 .—尾注]

我了解他们在注释中描述的内容如下:

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)此外,[Note:...]从句中的括号使我们似乎可以从之前的句子中弄清楚。那是标准的哪一部分?

3)这是否意味着右值引用绑定不是我们期望纯右值表达式(在右侧)的上下文?

4) 与其他转换一样,glvalue 到prvalue 的转换是否涉及运行时的工作,可以让我观察到它?

我在这里的目的不是问是否需要允许这样的转换。我正在尝试学习以标准为起点向自己解释这段代码的行为。

一个好的答案是通过我上面的引用并解释(基于解析文本)其中的注释是否也隐含在其文本中。然后它可能会添加任何其他引号,让我知道这种转换可能无法隐式发生的其他上下文,或者解释没有更多这样的上下文。也许一般讨论为什么将 glvalue 到 prvalue 视为转换。

4

2 回答 2

40

我认为左值到右值的转换不仅仅是在需要右值的地方使用左值。它可以创建一个类的副本,并且总是产生一个,而不是一个对象。

我将 n3485 用于“C++11”,将 n1256 用于“C99”。


对象和值

最简洁的描述在 C99/3.14 中:

目的

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

C++11/[intro.object]/1 中也有一点

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

所以一个对象包含一个值(可以包含)。


价值类别

尽管它的名字,值类别分类表达式,而不是值。左值表达式甚至不能被视为值。

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

以下是关于对象的部分:

  • 左值([...])指定函数或对象。[...]
  • 一个xvalue(一个“eXpiring”值)也指一个对象 [...]
  • 值(“广义”左值)是左值或 xvalue。
  • 右值([...]) 是一个xvalue、一个临时对象或它的子对象,或者一个与对象无关的值。
  • prvalue(“纯”右值)是不是 xvalue 的右值。[...]

请注意短语“与对象无关的值”。另请注意,由于 xvalue 表达式引用对象,真必须始终作为纯右值表达式出现。


左值到右值的转换

如脚注 53 所示,现在应将其称为“glvalue 到prvalue 转换”。首先,这是报价:

1 非函数、非数组类型的左值T可以转换为纯右值。如果T是不完整类型,则需要进行此转换的程序格式错误。如果泛左值所引用的对象不是类型对象,T也不是派生自 的类型的对象T,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。如果T是非类类型,则纯右值的类型是 的 cv 非限定版本T。否则,纯右值的类型是T

第一段规定了转换的要求和结果类型。它还不关心转换的效果(除了未定义的行为)。

2 当在未计算的操作数或其子表达式中发生左值到右值的转换时,不访问包含在引用对象中的值。否则,如果glvalue 具有类类型,则转换复制初始化Tglvalue 的临时类型,并且转换的结果是临时的prvalue。否则,如果泛左值具有(可能是 cv 限定的)类型std::nullptr_t,则纯右值结果是一个空指针常量。否则,glvalue指示的对象中包含的值就是prvalue结果。

我认为您会看到最常应用于非类类型的左值到右值转换。例如,

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结果。

因此,通常情况下,左值到右值的转换只会从 object 中读取值。这不仅仅是值(表达式)类别之间的无操作转换;它甚至可以通过调用复制构造函数来创建一个临时的。并且左值到右值的转换总是返回一个纯右,而不是一个(临时)对象

请注意,左值到右值的转换并不是将左值转换为纯右值的唯一转换:还有数组到指针的转换和函数到指针的转换。


值和表达式

大多数表达式不会产生对象[[citation needed]]。然而,一个id-expression可以是一个标识符,它表示一个实体。对象是一个实体,所以有一些表达式可以产生对象:

int x;
x = 5;

赋值表达式 的左侧x = 5也需要是一个表达式。x这是一个id-expression,因为x是一个标识符。此id 表达式的结果是由 表示的对象x

表达式应用隐式转换:[expr]/9

每当一个泛左值表达式作为一个操作数的操作数出现时,该操作数需要一个纯右值,左值到右值、数组到指针或函数到指针的标准转换将应用于将表达式转换为纯右值。

和 /10 关于通常的算术转换以及 /3 关于用户定义的转换。

我现在很想引用一个“期望该操作数的纯右值”的运算符,但除了强制转换之外找不到任何东西。例如,[expr.dynamic.cast]/2 “如果T是指针类型,v[操作数] 应该是指向完整类类型的指针的纯右值”。

许多算术运算符所需的通常算术转换确实通过使用的标准转换间接调用左值到右值的转换。除了从左值转换为右值的三个标准转换之外,所有标准转换都需要纯右值。

然而,简单的赋值不会调用通常的算术转换。它在 [expr.ass]/2 中定义为:

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

因此,尽管它没有明确要求右侧的纯右值表达式,但它确实需要一个value。我不清楚这是否严格要求左值到右值的转换。有一个论点是访问未初始化变量的值应始终调用未定义的行为(另见CWG 616),无论是通过将其值分配给对象还是将其值添加到另一个值。但是这种未定义的行为只需要左值到右值的转换(AFAIK),这应该是访问存储在对象中的值的唯一方法。

如果这个更概念化的观点是有效的,我们需要左值到右值的转换来访问对象内部的值,那么理解它在哪里(并且需要)应用就会容易得多。


初始化

与简单赋值一样,有一个讨论是否需要左值到右值的转换来初始化另一个对象:

int x = 42; // initializer is a non-string literal -> prvalue
int y = x;  // initializer is an object / lvalue

对于基本类型,[dcl.init]/17 最后一个要点说:

否则,被初始化对象的初始值是初始化表达式的(可能转换的)值。如有必要,将使用标准转换将初始化表达式转换为目标类型的 cv 非限定版本;不考虑用户定义的转换。如果无法完成转换,则初始化格式错误。

但是,它也提到了初始化表达式的值。与 simple-assignment-expression 类似,我们可以将其视为对左值到右值转换的间接调用。


参考绑定

如果我们将左值到右值的转换视为访问对象值的一种方式(加上为类类型操作数创建临时对象),我们就会理解它通常不适用于绑定到引用:引用是左值,它总是指一个对象。因此,如果我们将值绑定到引用,我们需要创建临时对象来保存这些值。如果引用的初始化表达式是纯右值(它是一个值或临时对象),情况确实如此:

int const& lr = 42; // create a temporary object, bind it to `r`
int&& rv = 42;      // same

禁止将纯右值绑定到左值引用,但具有产生左值引用的转换函数的类类型的纯右值可以绑定到转换后类型的左值引用。

[dcl.init.ref] 中对引用绑定的完整描述相当长而且离题。我认为与这个问题有关的本质是引用指的是对象,因此没有glvalue-to-prvalue(object-to-value)转换。

于 2014-01-08T15:11:42.797 回答
4

关于glvalues:glvalue(“广义”左值)是一个表达式,它可以是左值也可以是xvalue。通过左值到右值、数组到指针或函数到指针的隐式转换,可以将左值隐式转换为纯右值。

当在需要右值(例如数字)的上下文中使用左值参数(例如对对象的引用)时,将应用左值转换。

左值到右值转换
任何非函数、非数组类型 T 的左值可以隐式转换为相同类型的纯右值。如果 T 是非类类型,则此转换还会删除 cv 限定符。除非在未计算的上下文中遇到(在 sizeof、typeid、noexcept 或 decltype 的操作数中),此转换使用原始 glvalue 作为构造函数参数有效地复制构造类型 T 的临时对象,并且该临时对象作为纯右值返回. 如果 glvalue 的类型为 std::nullptr_t,则生成的纯右值是空指针常量 nullptr。

于 2014-01-08T07:44:32.397 回答