我认为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
int&& rv = 42
禁止将prvalue绑定到lvalue引用上,但是具有返回lvalue引用的转换函数的类类型的prvalue可以绑定到转换类型的lvalue引用上。
[dcl.init.ref]中对于引用绑定的完整描述相当冗长且与本问题无关。我认为与本问题相关的本质是引用是指向对象的,因此不进行glvalue-to-prvalue(对象到值)转换。
int && y = x;
不是一个表达式,而是一个声明。因此,“等号右侧的操作数”不会自动应用lvalue-to-rvalue转换。 - dypint && x = 1; ++x;
实际上并不更改1
的值。但是int y = 42; int &&x = std::move(y); ++x;
将更改y
的值。 - Casey