在C++中,用自身初始化全局变量是否具有未定义行为?

66
int i = i;

int main() { 
 int a = a;
 return 0;
} 

int a = a 肯定具有未定义行为(UB),更多细节可以在 Is reading an uninitialized value always an undefined behaviour? Or are there exceptions to it? 中找到。

但是 int i = i 呢?在C++中,我们可以将非常量值分配给全局变量。由于它具有文件作用域,因此在遇到声明之前已经被声明并初始化为零。在这种情况下,我们稍后在定义中将其赋值为 0。可以安全地说这不会产生 UB 吗?


4
没问题,因为静态存储期对象在任何其他初始化之前都会进行零初始化,所以这是安全的。 - M.M
"文件作用域"是C语言中的一个概念。在C++中对应的概念是"命名空间作用域"。 - Ruslan
初始化器具有对正在初始化的标识符的可见性,原因是为了使递归/循环引用成为可能,例如 struct circular_list x = { &x, &x }。这就是它的作用。 - Kaz
3个回答

59

令人惊讶的是,这并不是未定义行为。

静态初始化 [basic.start.static]

如果具有静态或线程存储期的变量或临时对象进行了常量初始化,则执行常量初始化。如果未执行常量初始化,则具有静态存储期或线程存储期的变量将被初始化为零。零初始化和常量初始化一起称为静态初始化;所有其他初始化都是动态初始化。所有静态初始化在任何动态初始化之前强烈发生

重要部分加粗。 "静态初始化" 包括全局变量初始化,"静态存储期" 包括全局变量,并且上述条款适用于此处:

int i = i;

这不是常量初始化。因此,根据上述条款进行零初始化(对于基本整数类型,零初始化意味着将其设置为0)。

上述条款还指定,在动态初始化之前必须进行零初始化。

因此,这里会发生什么:

  1. i初始化为0。
  2. 然后,i从自身进行动态初始化,因此它仍然保持0。

38
太好笑了。这让你创建一个不可构造的对象! struct S { S() = delete; } s = s; - Raymond Chen
9
针对没有默认构造函数的 C++20 之前版本中的无捕获 lambda 表达式,这篇文章非常实用:https://dev59.com/aLXna4cB1Zd3GeqPEQ-N#57012585。 - Justin
2
全局别名该如何改变? int & i = i; - Vincent Fourmond
1
但是,i 的生命周期何时开始?当静态初始化完成时还是动态初始化完成时?如果是前者,那么例如全局的 std::string s; 呢?在动态初始化完成之前尝试从中读取是否会导致未定义行为? - HolyBlackCat
1
我们不知道在动态时间i是否仍然为零,因为全局构造函数可能已经改变了它,对吧?就好像在动态初始化时发生了一个赋值i = i。由于这是一个无操作,它可以被优化掉。 - Kaz
显示剩余14条评论

5
行为可能对i未定义,因为根据您读取标准的方式,您可能会在i的生命周期开始之前读取它。
引用:

[basic.life]/1.2

……类型为T的对象的生命周期从以下时刻开始:

— 它的初始化(如果有)完成……

如其他答案中所述,i被初始化两次:首先静态地进行零初始化,然后动态地使用i进行初始化。
哪个初始化开始生命周期?第一个还是最终的?
标准表述模糊,并且其中存在冲突的注释(尽管所有这些都不是规范性的)。首先,在[basic.life]/6感谢@eerorika)中有一个脚注,明确表示动态初始化开始生命周期:

[basic.life]/6

在对象的生命周期开始之前,但对象将占用的存储空间已经被分配后26

...

26) 例如,在具有静态存储期的对象动态初始化之前...

这个解释对我来说最有意义,因为否则就可以在对象进行动态初始化之前访问类实例,这样它们就无法建立其不变性(包括标准库类)。

还有一个[basic.start.static]/3中存在的矛盾说明, 但那个说明比我上面提到的说明要旧。


1
有趣的是,C++17和C++20之间lifetime的定义发生了变化,因此这个脚注也进行了修改。这与[basic.start.dynamic]/5中的example相矛盾,但是例子并不具有规范性,很可能只是漏掉了更新这个例子。 - aschepler
4
那个问题是关于 C 语言的,int i; i = i; 在命名空间范围内甚至都不合法。 - aschepler
1
如果说有什么的话,我认为这个(非规范性的)脚注与 [basic.start.static]/3 中的(非规范性的)注释是相矛盾的。 - dfrib
@dfrib 啊,已经编辑了。 - HolyBlackCat
定义已经改变了...这是有道理的,因为现在我们有了constexpr构造函数。因此,你不能仅仅说原始类型按一种方式工作,而具有构造函数的类型具有单独的存储分配和初始化。 - JDługosz
显示剩余2条评论

1

在我看来,int i = i; 似乎具有未定义的行为,而不是由不确定的值引起的。术语“不确定的值”是为具有自动或动态存储期的对象设计的。

[basic.indet#1]

当获取具有自动或动态存储期限的对象的存储空间时,该对象具有不确定值,并且如果未对该对象执行初始化,则该对象保留不确定值,直到该值被替换([expr.ass])。

[basic.indet#2]

如果评估产生不确定值,则其行为未定义,除非以下情况...
在您的示例中,名为i的对象具有静态存储期,因此它不在讨论不确定值的范围内。这样的对象在任何动态初始化之前都有零初始化,如[basic.start.static#2]所述。
因此,它的初始值为零。当i用作初始化器来初始化自身时,它是动态初始化,遵循[dcl.init]。
除此之外,正在初始化的对象的初始值是初始化表达式(可能转换后)的值。
这违反了[basic.lifetime]中的规则。
如果: -glvalue用于访问对象,或者

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接