【问题标题】:Does initialization entail lvalue-to-rvalue conversion? Is `int x = x;` UB?初始化是否需要左值到右值的转换?是`int x = x;` UB吗?
【发布时间】:2023-04-10 01:54:02
【问题描述】:

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

int x = x;

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

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

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

【问题讨论】:

  • 我觉得行为很明确。 x 的值不会改变。但是,x 的值是未定义的。
  • @Bingo:如果您有这种感觉,您能否制定一个源自语言标准的论点并将其发布为答案? :-)
  • 不就是问int y; int x = y;是不是UB一样吗? [编辑:嗯,可能没有。这是关于计算 unitialized 变量的值,而不是 default-initialized one]
  • 我想知道x 的生命周期是否一定从它在右侧被评估的那一刻开始。是否指定左侧部分(我猜是为x 分配存储空间)在右侧部分之前排序?
  • @AndyProwl:自动存储分配根本没有排序。只有表达式评估是可以排序的。声明语句不是表达式。

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


【解决方案1】:

更新: 根据 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”这个词。上面的段落提到将使用标准转换“如果需要”转换为目标类型,但没有说明类别转换:

  1. 是否会根据需要进行类别转换?
  2. 需要它们吗?

关于第二个问题,正如答案的原始部分所讨论的那样,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 reportJesse 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结果。

【讨论】:

  • 嗯,你的帖子里有很多关于“运营商”的话题,但我的问题与运营商无关......
  • @KerrekSB:是的,我知道这一点。这就是为什么我将我的答案标记为“猜想”。我的假设是,就像未为运算符指定值类别要求一样,未指定初始化器的值类别要求。并且由于运算符的预期规范是(编辑:似乎是),只要需要一个值,除非另有说明,否则需要一个纯右值,因此 IMO 对初始化器做出相同的假设是有意义的。恐怕无法对您的问题给出纯粹正式的答案,因为标准本身缺乏明确定义的规范。
  • +1,显然有用,虽然我不知道猜想是否正确。
  • @jogojapan:谢谢。我也不是,所以我当然称它为猜想;-) 但是,恕我直言,假设它是真的比假的更有意义。
  • 好的,已删除。此外,与defect report 616 及其相关问题略有相关,但 AFAICT 它不涵盖 OP 的情况。
【解决方案2】:

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

T t = e;

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

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

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

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

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

【讨论】:

  • (我很想给你一个赏金,但我可以给的最低赏金是 300 -- 我是小气还是便宜?:-))
  • 查看this proposal
  • @kerrek 我已经知道那个提议了。他们正在制定更清晰的规则而不是使用弱英语的随意术语,这很好。
【解决方案3】:

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

【讨论】:

  • 在自动输入的情况下是一个错误。 ` 在其自己的初始化程序中使用具有 'auto' 类型的变量 'auto x' `
  • @Arpit:问题中没有auto(这不是“自动”的意思!)。
  • 哦!我只是考虑自动输入自动变量。我的错误
  • @Arpit: auto 不是类型。这是一个关键字。
  • @KerrekSB 别那么认真。:) 我知道它是一个类型说明符。
【解决方案4】:

行为不是未定义的。该变量是未初始化的,并与任何随机值未初始化的值开始时保持一致。来自 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 明确地对此案例进行测试。

【讨论】:

  • 根据 C++ 标准,行为未定义。这意味着编译器可以做他们喜欢做的事情,而您的示例显示了 clang 选择做的事情。
  • 变量未初始化,并保持与任何随机值未初始化值开始 ...不,编译器可以做任何事情,包括优化代码,请参阅@987654322 @.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-08
  • 2011-07-03
  • 2020-07-14
  • 1970-01-01
  • 2022-09-28
  • 1970-01-01
相关资源
最近更新 更多