57

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

int x = x;

x用自己初始化,它(作为原始类型)未初始化,因此具有不确定的值(假设它是一个自动变量)。

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

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

4

4 回答 4

22

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


免责声明我承认这个答案相当投机。另一方面,当前 C++11 标准的表述似乎不允许更正式的答案。


本问答的上下文中,C++11 标准未能正式指定每种语言结构所期望的值类别。在下文中,我将主要关注内置运算符,尽管问题是关于初始化程序的。最终,我会将我对运算符的情况得出的结论扩展到初始化程序的情况。

在内置运算符的情况下,尽管缺乏正式的规范,但在标准中发现(非规范性)证据表明,预期的规范是让prvalues在需要值的地方被预期,当没有指定时否则

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

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

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

因此,函数调用不应干预左值到右值的转换以及与任何单个复合赋值运算符相关的副作用

当然,如果没有预期的右值,这个注释将毫无意义。

正如Johannes Schaub在对链接问答的评论中指出的那样,在 4/8 中发现了另一个证据:

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

这似乎意味着左值到右值的转换是在内置运算符的所有操作数上执行的,除非另有说明。反过来,这意味着除非另有说明,否则右值应作为内置运算符的操作数。


推测:

尽管初始化不是赋值,因此运算符不参与讨论,但我怀疑规范的这个区域受到上述相同问题的影响。

甚至可以在第 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

这种不统一的行为对我来说没有多大意义。IMO 更有意义的是,无论哪里需要一个值,都需要一个纯右值。

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

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

然而,虽然 Jesse 主要关注“如果需要”这一点,但我也想强调“类型”这个词。上面的段落提到“如果需要”将使用标准转换来转换为目标类型,但没有说明类别转换:

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

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

至于第一个问题,我认为答案也是“是”似乎是合理的。如果它是“否”,那么显然正确的程序将是格式错误的:

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",我们应该有 Undefined Behavior


进一步的证据:

缺陷报告(由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结果。

于 2013-02-20T23:20:02.330 回答
7

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

T t = e;

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

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

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

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

每当一个泛右值出现在预期纯右值的上下文中时,该泛左值就被转换为一个纯右值;见 4.1、4.2 和 4.3。

于 2013-07-07T16:52:31.880 回答
-5

这不是未定义的行为。您只是不知道它的具体值,因为没有初始化。如果变量是全局和内置类型,那么编译器会将其初始化为正确的值。如果变量是本地的所以编译器不会初始化它,所以所有的变量都是自己初始化的,不要依赖编译器。

于 2013-02-18T12:46:26.950 回答
-6

行为不是未定义的。该变量是未初始化的,并且与任何随机值未初始化的值开始时保持一致。来自 clan'g 测试服的一个例子:

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中找到该案例的明确测试。

于 2013-02-26T23:16:47.873 回答