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

时间:2013-02-18 11:55:30

标签: c++ initialization undefined-behavior language-lawyer

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

int x = x;

这会自己初始化x,其中(作为基本类型)未初始化,因此具有不确定的值(假设它是自动变量)。

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

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

4 个答案:

答案 0 :(得分:20)

更新: 根据评论中的讨论,我在此答案的最后添加了更多证据。


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


this Q&A 的上下文中,已经发现C ++ 11标准无法正式指定 value categories 的预期值每种语言结构。在下文中,我将主要关注内置运算符,尽管问题是关于初始值设定项。最后,我将最终将运算符的结论扩展到初始化器的情况。

对于内置运算符,尽管缺乏正式的规范,但标准中的(非规范性)证据表明预期的规范要让在需要值的任何地方都可以使用prvalues,如果没有另外指定

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

  

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

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

  

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

当然,如果没有预期的左值,那么这张纸就没有意义了。

正如Johannes Schaub在关联Q& A的评论中指出的那样,在4/8中找到了另一个证据:

  

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

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


<强>猜想:

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

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

  

不需要通常左值到右值(4.1),数组到指针(4.2)和函数到指针(4.3)标准转换,因此被抑制,当这种直接绑定到左值时。

&#34;通常&#34;似乎暗示在初始化不属于引用类型的对象时,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

这种不统一的行为对我来说没有多大意义。让IMO更有意义的是,只要需要一个值,就可以得到一个prvalue。

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

  

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

然而,虽然Jesse主要关注&#34; 如果有必要&#34;我还要强调一下&#34; 类型&#34;。上面的段落提到标准转换将被使用&#34; 如果有必要&#34;转换为目标类型,但没有说明类别转化:

  1. 如果需要,是否会执行类别转换?
  2. 他们需要吗?
  3. 对于第二个问题,正如答案的原始部分所讨论的那样,C ++ 11标准目前没有指定是否需要类别转换,因为没有提到复制初始化是否需要一个prvalue作为初始化者。因此,不可能给出一个明确的答案。但是,我相信我提供了足够的证据来假设这是预期的规范,因此答案是&#34;是&#34;。

    至于第一个问题,对我来说似乎是合情合理的答案是&#34;是&#34;同样。如果它是&#34;否&#34;,显然正确的程序将是不正确的:

    int y = 0;
    int x = y; // y is lvalue, prvalue expected (assuming the conjecture is correct)
    

    总结(A1 =&#34; 回答问题1 &#34;,A2 =&#34; 回答问题2 &#34 ):

              | A2 = Yes   | A2 = No |
     ---------|------------|---------|
     A1 = Yes |     UB     |  No UB  | 
     A1 = No  | ill-formed |  No UB  |
     ---------------------------------
    

    如果A2是&#34;否&#34;,A1无关紧要:没有UB,但是第一个例子的奇怪情况(例如z = y给出UB,但不是{{ 1}}即使z = x)出现了。如果A2是&#34;是&#34;,另一方面,A1变得至关重要;然而,已经有足够的证据证明它会是&#34;是&#34;。

    因此,我的论点是A1 =&#34;是&#34;和A2 =&#34;是&#34;,我们应该有未定义的行为


    进一步证据:

    defect report (由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是一元&amp;的操作数。运算符或绑定到引用,结果是未指定的值。 [脚注:每次将左值到右值转换应用于对象时,该值可能不同。具有分配给寄存器的不确定值的unsigned char对象可能会陷阱。 - 尾注]

         

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

         

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

答案 1 :(得分:7)

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

进行定义
T t = e;
  

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

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

  

如果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。

答案 2 :(得分:-4)

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

答案 3 :(得分:-5)

行为未定义。该变量未初始化,并保持未初始化值启动的任何随机值。 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测试中明确找到此案例。