初始化是否需要左值到右值的转换?是`int x = x;`UB?

Ker*_* SB 55 c++ initialization undefined-behavior language-lawyer

C++标准包含一个半着名的例子,在3.3.2中的"令人惊讶的"名称查找,"声明点":

int x = x;
Run Code Online (Sandbox Code Playgroud)

这初始化x自身,(原始类型)未初始化,因此具有不确定的值(假设它是一个自动变量).

这实际上是未定义的行为吗?

根据4.1"左值到右值转换",对未初始化的值执行左值到右值的转换是未定义的行为.右手是否x接受这种转换?如果是这样,示例实际上会有未定义的行为吗?

And*_*owl 20

更新: 在评论中讨论后,我在这个答案的最后添加了一些证据.


免责声明:我承认这个答案相当具有推测性.另一方面,目前C++ 11标准的制定似乎不允许更正式的答案.


本问答环节中,已经发现C++ 11标准未能正式指定每种语言结构所期望的值类别.在下文中,我将主要关注内置运算符,尽管问题是关于初始化器.最后,我将最终将运算符的结论扩展到初始化器的情况.

在内置运算符的情况下,尽管缺乏正式的规范,但在标准中发现了(非规范的)证据,即预期的规范是在需要值的任何地方都要求prvalues,并且当没有指定时否则.

例如,第3.10/1段中的注释说:

第5章中对每个内置运算符的讨论表明了它产生的值的类别以及它所期望的操作数的值类别.例如,内置赋值运算符期望左操作数是左值,右操作数是prvalue并产生左值作为结果.用户定义的运算符是函数,它们期望和产生的值的类别由它们的参数和返回类型决定

另一方面,关于赋值运算符的第5.17节没有提到这一点.但是,在附注(第5.17/1段)中再次提到了执行左值到右值转换的可能性:

因此,函数调用不应介入左值到右值的转换和与任何单个复合赋值运算符相关的副作用.

当然,如果不期望rvalue,那么这个注释将毫无意义.

正如Johannes Schaub在关于问答的评论中指出的那样,另一个证据是在4/8中找到:

在某些情况下,某些转换会被抑制.例如,在一元&运算符的操作数上不进行左值到右值的转换.在这些运算符和上下文的描述中给出了特定的例外.

这似乎暗示了对内置运算符的所有操作数执行左值到右值的转换,除非另有说明.反过来,这将意味着rvalues被期望作为内置运算符的操作数,除非另有说明.


推测:

即使初始化不是赋值,因此运算符也没有进入讨论,我怀疑规范的这个区域受到上述同样问题的影响.

即使在第8.5.2/5段中,关于引用的初始化(不需要左值初始化表达式的值),也可以找到支持这种信念的跟踪:

通常的左值到右值(4.1),阵列到指针(4.2),和功能到指针(4.3)标准转换是不需要的,并且因此被抑制,当这种直接绑定到左值完成.

"通常"这个词似乎意味着在初始化不属于引用类型的对象时,lvalue-to-rvalue转换意味着适用.

因此,我认为尽管对初始化器的预期值类别的要求是不明确的(如果不是完全缺失),但基于所提供的证据,假设预期的规范是这样的:

只要语言结构需要值,除非另有说明,否则预期值为prvalue.

在此假设下,您的示例中将需要左值到右值的转换,这将导致未定义的行为.


其他证据:

为了提供进一步的证据来支持这个猜想,我们假设它是错误的,因此复制初始化确实不需要左值到右值的转换,并考虑以下代码(感谢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
Run Code Online (Sandbox Code Playgroud)

这种不统一的行为对我来说并没有多大意义.让IMO更有意义的是,无论何处需要价值,都需要预付款.

此外,正如Jesse Good在答案中正确指出的那样,C++标准的关键段落是8.5/16:

- 否则,初始化对象的初始值是初始化表达式(可能已转换)值.如有必要,将使用标准转换(第4节)将初始化表达式转换为目标类型的cv非限定版本; 不考虑用户定义的转换.如果无法进行转换,则初始化不正确.[注意:类型"cv1 T"的表达式可以独立于cv限定符cv1和cv2初始化类型为"cv2 T"的对象.

然而,虽然Jesse主要关注" 如果必要 "这一点,我还要强调" 类型 " 这个词.上面的段落提到标准转化将在" 必要时 "用于转换为目标类型,但没有说明类别转化:

  1. 是否需要执行类别转换?
  2. 他们需要吗?

对于第二个问题,正如答案的原始部分所讨论的那样,C++ 11标准目前没有指定是否需要类别转换,因为没有提到复制初始化是否需要将prvalue作为初始化程序.因此,不可能给出一个明确的答案.但是,我相信我提供了足够的证据来假设这是预期的规范,所以答案是"是".

至于第一个问题,对我来说似乎也是合理的,答案也是"是".如果它是"否",显然正确的程序将是不正确的:

int y = 0;
int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct)
Run Code Online (Sandbox Code Playgroud)

总结一下(A1 =" 对问题1的回答 ",A2 =" 对问题2的回答 "):

          | A2 = Yes   | A2 = No |
 ---------|------------|---------|
 A1 = Yes |     UB     |  No UB  | 
 A1 = No  | ill-formed |  No UB  |
 ---------------------------------
Run Code Online (Sandbox Code Playgroud)

如果A2为"否",则A1无关紧要:没有UB,但第一个例子的奇怪情况(例如z = y给予UB,但不是z = x即使x == y)显示出来.如果A2为"是",另一方面,A1变得至关重要; 然而,已经有足够的证据证明它将是"是".

因此,我的论点是A1 ="是"而A2 ="是",我们应该有未定义的行为.


进一步的证据:

缺陷报告(由Jesse Good提供)提出了一个旨在在此情况下提供未定义行为的更改:

[...]另外,4.1 [conv.lval]第1段说将左值到右值的转换应用于"未初始化的对象"会导致未定义的行为; 这应该用具有不确定值的对象来重新定义.

特别是,第4.1段的拟议措辞是:

当在未评估的操作数或其子表达式(条款5 [expr])中发生左值到右值转换时,不访问引用对象中包含的值.在所有其他情况下,转换结果根据以下规则确定:

- 如果T是(可能是cv限定的)std :: nullptr_t,则结果是空指针常量(4.10 [conv.ptr]).

- 否则,如果glvalue T具有类类型,则转换从glvalue初始化类型T的临时值,转换结果是临时值的prvalue.

- 否则,如果glvalue引用的对象包含无效指针值(3.7.4.2 [basic.stc.dynamic.deallocation],3.7.4.3 [basic.stc.dynamic.safety]),则行为是实现定义的.

- 否则,如果T是一个(可能是cv限定的)无符号字符类型(3.9.1 [basic.fundamental]),并且glvalue引用的对象包含一个不确定的值(5.3.4 [expr.new],8.5 [dcl.init],12.6.2 [class.base.init]),并且该对象没有自动存储持续时间,或者glvalue是一元&运算符的操作数,或者它被绑定到引用,结果是未指明的价值.[脚注:每次将左值到右值转换应用于对象时,该值可能不同.具有分配给寄存器的不确定值的unsigned char对象可能会陷阱. - 尾注]

- 否则,如果glvalue引用的对象包含不确定的值,则行为未定义.

- 否则,如果glvalue有(可能是cv-qualified)类型std :: nullptr_t,则prvalue结果是一个空指针常量(4.10 [conv.ptr]).否则,glvalue指示的对象中包含的值是prvalue结果.


Joh*_*itb 7

e类型的表达式的隐式转换序列T被定义为等效于以下声明,使用t转换的结果(模数值类别,将根据其定义T),4p3和4p6

T t = e;
Run Code Online (Sandbox Code Playgroud)

任何隐式转换的效果与执行相应的声明和初始化相同,然后使用临时变量作为转换的结果.

在第4节中,将表达式转换为类型总是会产生具有特定属性的表达式.例如,转换0int*产生空指针值,而不仅仅是一个任意指针值.值类别也是表达式的特定属性,其结果定义如下

如果T是左值引用类型或函数类型的右值引用(8.3.2),则结果是左值;如果T是对象类型的右值引用,则为xvalue,否则为prvalue.

因此我们知道int t = e;转换序列的结果是prvalue,因为int它是非引用类型.因此,如果我们提供glvalue,我们显然需要转换.3.10p2进一步澄清,毫无疑问

每当glvalue出现在期望prvalue的上下文中时,glvalue就会转换为prvalue; 见4.1,4.2和4.3.