更新: 根据 cmets 中的讨论,我在此答案的末尾添加了更多证据。
免责声明:我承认这个答案相当投机。另一方面,当前 C++11 标准的表述似乎不允许更正式的答案。
在 this Q&A 的上下文中,C++11 标准未能正式指定每种语言结构所期望的 value categories。在下文中,我将主要关注 内置运算符,尽管问题是关于 initializers。最后,我会将我对运算符的情况得出的结论扩展到初始化程序的情况。
在内置运算符的情况下,尽管缺乏正式规范,但在标准中发现(非规范)证据表明意图规范是让prvalues 在任何需要值的地方都需要,如果没有另外指定。
例如,第 3.10/1 段中的注释说:
第 5 章中对每个内置运算符的讨论指出了它产生的值的类别以及它所期望的操作数的值类别。例如,内置赋值运算符期望左操作数是左值,右操作数是纯右值并产生左值作为结果。用户定义的运算符是函数,类别他们期望的值和产量由他们的参数和返回类型决定
另一方面,关于赋值运算符的第 5.17 节没有提到这一点。但是,再次在注释(第 5.17/1 段)中提到了执行左值到右值转换的可能性:
因此,函数调用不应干预左值到右值的转换和与任何单个复合赋值运算符相关的副作用
当然,如果不期望右值,则此注释将毫无意义。
在 4/8 中发现了另一个证据,正如 Johannes Schaub 在链接问答的 cmets 中指出的那样:
在某些情况下,某些转化会被抑制。例如,左值到右值的转换不是在一元 & 运算符的操作数上完成的。在这些运算符和上下文的描述中给出了特定的例外情况。
这似乎暗示左值到右值的转换是在内置运算符的所有操作数上执行的,除非另有说明。反过来,这意味着 除非另有说明,否则期望右值作为内置运算符的操作数。
猜想:
尽管初始化不是赋值,因此运算符不参与讨论,但我怀疑规范的这个区域受到上述相同问题的影响。
支持这一信念的痕迹甚至可以在第 8.5.2/5 段中找到,关于 references 的初始化(不需要左值初始化表达式的值):
不需要通常的左值到右值(4.1)、数组到指针(4.2)和函数到指针(4.3)标准转换,因此被禁止,当这种对左值的直接绑定完成时。
“通常”一词似乎暗示在初始化非引用类型的对象时,将应用左值到右值的转换。
因此,我认为尽管对初始化器的期望值类别的要求没有明确规定(如果不是完全缺失的话),但基于所提供的证据,假设 有意规范是:
只要语言结构需要一个值,除非另有说明,否则需要一个纯右值。
在这种假设下,在您的示例中需要进行左值到右值的转换,这会导致未定义的行为。
其他证据:
只是为了提供进一步的证据来支持这个猜想,让我们假设它错误,因此复制初始化确实不需要左值到右值的转换,并考虑以下代码(感谢jogojapan 贡献):
int y;
int x = y; // No UB
short t;
int u = t; // UB! (Do not like this non-uniformity, but could accept it)
int z;
z = x; // No UB (x is not uninitialized)
z = y; // UB! (Assuming assignment operators expect a prvalue, see above)
// This would be very counterintuitive, since x == y
这种不统一的行为对我来说没有多大意义。 IMO 更有意义的是,只要需要一个值,就需要一个纯右值。
此外,正如Jesse Good 在他的回答中正确指出的那样,C++ 标准的关键段落是 8.5/16:
——否则,被初始化对象的初始值为
(可能已转换)初始化表达式的值。标准
如有必要,将使用转换(第 4 条)来转换
初始化表达式为 cv 非限定版本
目的地类型;不考虑用户定义的转换。如果
无法进行转换,初始化格式错误。 [ 笔记:
“cv1 T”类型的表达式可以初始化“cv2 T”类型的对象
独立于 cv 限定符 cv1 和 cv2。
然而,虽然 Jesse 主要关注“if necessary”这一点,但我还想强调“type”这个词。上面的段落提到将使用标准转换“如果需要”转换为目标类型,但没有说明类别转换:
- 是否会根据需要进行类别转换?
- 需要它们吗?
关于第二个问题,正如答案的原始部分所讨论的那样,C++11标准目前没有指定是否需要类别转换,因为没有提到复制初始化是否需要prvalue作为初始化器。因此,不可能给出明确的答案。但是,我相信我提供了足够的证据来假设这是预期规范,因此答案将是“是”。
至于第一个问题,我认为答案也是“是”是合理的。如果它是“否”,那么显然正确的程序将是格式错误的:
int y = 0;
int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct)
总结一下(A1 = "第 1 题答案", A2 = "第 2 题答案"):
| A2 = Yes | A2 = No |
---------|------------|---------|
A1 = Yes | UB | No UB |
A1 = No | ill-formed | No UB |
---------------------------------
如果 A2 为“否”,A1 无关紧要:没有 UB,但第一个示例的奇怪情况(例如,z = y 提供 UB,但不是 z = x,即使 x == y)出现。另一方面,如果 A2 为“是”,则 A1 变得至关重要;然而,已经有足够的证据证明它会是“是”。
因此,我的论点是 A1 = "Yes" 和 A2 = "Yes",我们应该有未定义的行为。
进一步的证据:
defect report(Jesse Good 提供)提出了一项旨在在这种情况下提供未定义行为的更改:
[...] 此外,4.1 [conv.lval] 第 1 段说,将左值到右值的转换应用于“未初始化的对象”会导致未定义的行为; 这应该用具有不确定值的对象来改写。
特别是,第 4.1 段的拟议措辞说:
当在未计算的操作数或其子表达式中发生左值到右值的转换(第 5 条 [expr])时,不会访问包含在引用对象中的值。在所有其他情况下,转换的结果根据以下规则确定:
——如果 T 是(可能是 cv 限定的)std::nullptr_t,则结果是一个空指针常量(4.10 [conv.ptr])。
— 否则,如果泛左值 T 具有类类型,则转换从泛泛值复制初始化类型 T 的临时值,并且转换的结果是临时值的纯右值。
— 否则,如果泛泛值引用的对象包含无效的指针值(3.7.4.2 [basic.stc.dynamic.deallocation]、3.7.4.3 [basic.stc.dynamic.safety]),则行为为实现定义。
— 否则,如果 T 是(可能是 cv 限定的)无符号字符类型(3.9.1 [basic.fundamental]),并且泛左值所指的对象包含不确定值(5.3.4 [expr.new ]、8.5 [dcl.init]、12.6.2 [class.base.init]),并且该对象没有自动存储持续时间,或者泛左值是一元 & 运算符的操作数,或者它被绑定到一个引用,结果是一个未指定的值。 [脚注:每次将左值到右值转换应用于对象时,该值可能不同。分配给寄存器的具有不确定值的 unsigned char 对象可能会陷入陷阱。 ——结束脚注]
— 否则,如果泛左值引用的对象包含不确定值,则行为未定义。
——否则,如果泛左值具有(可能是 cv 限定的)类型 std::nullptr_t,则纯右值结果是一个空指针常量(4.10 [conv.ptr])。否则,glvalue指示的对象中包含的值就是prvalue结果。