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

C ++标准在第3.3.2节“声明点”中包含了一个“令人惊讶的”名称查找的半名示例:

int x = x; 

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

这实际上是不确定的行为?

根据4.1“左值到右值的转换”,对未初始化的值执行左值到右值的转换是未定义的行为。 右边的x是否经历了这种转换? 如果是这样,这个例子实际上是否有未定义的行为?

更新: 在评论的讨论之后,我在这个答案的最后添加了更多的证据。


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


这个Q&A的背景下,出现了C ++ 11标准没有正式指定每个语言结构所期望的值类别 。 在下文中,我将主要关注内置运算符 ,但问题是关于初始化方法 。 最终,我最终会把我为经营者的案例得出的结论扩展到初始者案例。

对于内置运算符,尽管缺少正式的规范,但是在标准中可以找到(非规范的)证据, 预期的规范是在需要价值的地方预计价值,当没有指定时否则

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

第5章讨论每个内置操作符指出了它产生的值的类别和它期望的操作数的值类别。 例如, 内置赋值运算符期望左操作数是左值,右操作数是前值,并产生左值作为结果。 用户定义的运算符是函数,它们期望的值的类别和产出取决于它们的参数和返回类型

另一方面,关于作业操作员的第5.17节没有提到这一点。 但是,在一个注释(第5.17 / 1段)中提到了进行左值到右值转换的可能性:

因此,函数调用不应干涉左值与右值转换和与任何单一复合赋值运算符相关的副作用

当然,如果没有预期的话,这张纸条就没有意义了。

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

有一些情况下某些转换被抑制。 例如,左值到右值的转换不是在一元&运算符的操作数上完成的。 这些操作符和上下文的描述中给出了具体的例外。

这似乎暗示左值到右值的转换是在内建运算符的所有操作数上执行的,除非另有指定。 这就意味着, 除非另有规定,否则预期右值将作为内置运算符的操作数。


推测:

即使初始化不是分配,因此操作员也不参与讨论,但是我怀疑规范的这个领域是受上述同样的问题影响的。

支持这种信念的痕迹甚至可以在8.5.2 / 5节中找到,关于引用的初始化(不需要左值初始值表达式的值):

通常的左值到右值(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 

这种不一致的行为对我来说没有什么意义。 国际海事组织更有意义的是,无论在哪里需要价值,预计都会有一个值。

而且,正如Jesse Good在他的回答中正确指出的,C ++标准的关键段落是8.5 / 16:

– 否则,被初始化的对象的初始值是初始化表达式(可能被转换的)值如有必要 ,将使用标准转换(第4章)将初始化表达式转换为目标类型的cv不合格版本; 没有考虑用户定义的转换。 如果转换不能完成,则初始化不合格。 [注意:类型“cv1 T”的表达式可以独立于cv-qualifiers cv1和cv2来初始化类型为“cv2 T”的对象。

但是,杰西主要侧重于“ 如果有必要 ”一点,我还要强调“ 类型 ”一词。 上面的段落提到标准转换将被用于“ 如果需要 ”转换为目的地类型 ,但没有说明类别转换的任何内容:

  1. 如果需要,将执行类别转换吗?
  2. 他们需要吗?

关于第二个问题,正如答案的原始部分所讨论的那样,C ++ 11标准目前并没有指定是否需要类别转换,因为在这里没有提到copy-initialization是否需要一个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 =“是”,A2 =“是”,我们应该有未定义的行为


进一步的证据:

这个缺陷报告 ( Jesse Good提供 )提出了一个旨在给出未定义行为的变化:

此外,4.1 [conv.lval]第1段指出,将“左对右变换”应用于“未初始化的对象”会导致未定义的行为; 这应该用具有不确定价值的对象来重新表述

第4.1段的建议措词尤其是:

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

– 如果T(可能是cv-qualified)std :: nullptr_t,结果是一个空指针常量(4.10 [conv.ptr])。

– 否则,如果glvalue T有一个类的类型,转换拷贝 – 从glvalue初始化类型T的一个临时值,并且转换的结果是临时值。

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

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

否则,如果glvalue所引用的对象包含一个不确定的值,则行为是未定义的。

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

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

 T t = e; 

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

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

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

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

每当一个glvalue出现在一个prvalue预期的上下文中时,glvalue被转换为一个prvalue; 见4.1,4.2和4.3。

这不是不确定的行为。你只是不知道它的具体值,因为没有初始化。 如果变量是全局变量和内置类型,编译器会将其初始化为正确的值。 如果变量是局部的,所以编译器不能初始化它,所以所有的变量都是初始化的,不要依赖编译器。

行为不是未定义的。 该变量是未初始化的,并保留随初始化值随机的值。 来自氏族服装的一个例子是:

 int test7b(int y) { int x = x; // expected-note{{variable 'x' is declared here}} if (y) x = 1; // Warn with "may be uninitialized" here (not "is sometimes uninitialized"), // since the self-initialization is intended to suppress a -Wuninitialized // warning. return x; // expected-warning{{variable 'x' may be uninitialized when used here}} } 

你可以在clang / test / Sema / uninit-variables.c中找到这个测试。