【问题标题】:In C++, does initializing a global variable with itself have undefined behaviour?在 C++ 中,用自身初始化全局变量是否具有未定义的行为?
【发布时间】:2021-08-30 21:21:11
【问题描述】:
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++ 中,我们可以将非常量值分配给全局变量。 i 在遇到声明之前被声明并初始化为零(因为它具有文件范围)。在这种情况下,我们稍后在定义中将0 分配给它。 可以说这没有UB吗?

【问题讨论】:

  • 是的,正如你所说,这是安全的,因为静态存储持续时间的对象在任何其他初始化之前都是零初始化的
  • 文件范围是来自C的一个概念。C++中对应的概念是命名空间范围
  • 初始化程序对正在初始化的标识符具有可见性的原因是递归/循环引用可能像struct circular_list x = { &x, &x }。这就是它的用途。

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


【解决方案1】:

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

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

如果是变量或临时对象,则执行常量初始化 静态或线程存储持续时间是常量初始化的。 如果 不执行常量初始化,静态变量 存储持续时间或线程存储持续时间为零初始化。 一起称零初始化和常量初始化 静态初始化;所有其他初始化都是动态的 初始化。 所有静态初始化强烈发生在任何 动态初始化

重要部分加粗。 “静态初始化”包括全局变量初始化,“静态存储时长”包括全局变量,上述条款在这里适用:

int i = i;

这不是常量初始化。因此,根据上述子句进行零初始化(对于基本整数类型,零初始化意味着它被设置为 0,这并不奇怪)。上述子句还规定必须在动态初始化之前进行零初始化。

那么,这里发生了什么:

  1. i 被初始化为 0。
  2. i 然后从自身动态初始化,所以它仍然保持为 0。

【讨论】:

  • 太搞笑了。这使您可以创建一个不可构造的对象! struct S { S() = delete; } s = s;
  • @RaymondChen 实际上对 C++20 之前的无捕获 lambdas 很有用,它没有默认构造函数:stackoverflow.com/a/57012585/1896169
  • 全局别名会有什么变化? int & i = i;
  • 但是i 的生命周期从什么时候开始?静态初始化什么时候完成,还是动态初始化什么时候完成?如果是前者,那例如呢?全球std::string s;?是否在动态初始化完成之前尝试读取它而不是 UB?
  • 我们不知道i 在动态时间是否仍然为零,因为全局构造函数可能已经改变了它,对吧?就好像分配i = i 发生在动态初始化时。由于这是一个 noop,它可以被优化掉。
【解决方案2】:

i 的行为可能未定义,因为根据您阅读标准的方式,您可能会在其生命周期开始之前阅读 i

[basic.life]/1.2

... T 类型对象的生命周期开始于:

——它的初始化(如果有的话)已经完成......

正如另一个答案中提到的,i 被初始化了两次:第一次静态初始化为零,然后用 i 动态初始化。

哪个初始化开始生命周期?第一个还是最后一个?

标准含糊不清,其中有相互矛盾的注释(尽管都是are non-normative)。首先,[basic.life]/6 (thanks @eerorika) 中有一个脚注,明确表示动态初始化开始生命周期:

[basic.life]/6

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

...

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

这种解释对我来说最有意义,因为否则它会 在类实例经历动态之前访问它们是合法的 初始化,然后才能建立它们的不变量(包括标准定义的标准库类)。

还有一个conflicting note in [basic.start.static]/3,但那个比我上面提到的那个要老。

【讨论】:

  • 不,不是。 int i = i; 等于 int i; i = i;stackoverflow.com/a/67663586/14940626
  • 有趣。生命周期的定义在 C++17 和 C++20 之间发生了变化,这个脚注也随之改变。这确实与 [basic.start.dynamic]/5 中的 example 相矛盾,但示例也是非规范性的,他们似乎只是错过了更新此示例。
  • @Dan 这个问题是关于 C 的,int i; i = i; 在命名空间范围内甚至都不合法。
  • @aschepler 你能解释一下矛盾吗?
  • 如果有的话,我认为这个(非规范)脚注与[basic.start.static]/3 中的(非规范)注释相矛盾。
【解决方案3】:

在我看来int i = i; 具有未定义的行为,不是由不确定的值引起的。术语不确定值是为具有自动或动态存储持续时间的对象设计的。

[basic.indet#1]

当对象获得自动或动态存储时长的存储时,该对象有一个不确定值,如果没有对该对象进行初始化,则该对象保留一个不确定的值,直到该值被替换([expr.ass])。

[basic.indet#2]

如果评估产生不确定值,则行为未定义,但以下情况除外...

在您的示例中,名为i 的对象具有静态存储持续时间,因此它不在谈论不确定值的范围内。而且,根据 [basic.start.static#2]

,这样的对象具有在任何动态初始化之前发生的零初始化

零初始化和常量初始化合称为静态初始化;所有其他初始化都是动态初始化。所有静态初始化都强烈发生在 ([intro.races]) 任何动态初始化之前。

因此,它的初始值为零。当i 用作初始化器来初始化自身时。这是一个动态初始化,它服从 [dcl.init]。

否则,被初始化对象的初始值是初始化表达式的(可能转换的)值。

它违反了[basic.lifetime]中的规则

如果出现以下情况,则程序具有未定义的行为:

  • glvalue 用于访问对象,或

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-29
    相关资源
    最近更新 更多