考虑这个程序:
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
extern int i;
struct S {
S() {
if (i == 0) {
puts("Hello, world!");
exit(0);
}
}
};
S s;
int i = 1 + 2 * INT_MIN;
int main() { }
正如我理解表达式的评估,这是一个严格符合的程序,打印“Hello,world!”,然后退出,从不实际评估i
的初始化程序:
3.6.2非局部变量的初始化[basic.start.init]
[...]
具有静态存储持续时间(3.7.1)或线程存储持续时间(3.7.2)的变量在进行任何其他初始化之前应进行零初始化(8.5)。
执行常量初始化:
- [...]
- 如果具有静态或线程存储持续时间的对象未被构造函数调用初始化,并且其初始化程序中出现的每个完整表达式都是常量表达式。
一起,零初始化和常量初始化称为静态初始化;所有其他初始化都是动态初始化。在进行任何动态初始化之前,应执行静态初始化。具有静态存储持续时间的非局部变量的动态初始化是有序的或无序的。 [...]在单个翻译单元中定义的具有有序初始化的变量应按其在翻译单元中的定义顺序进行初始化。 [...]
5.19常量表达式[expr.const]
条件表达式是核心常量表达式,除非它涉及其中一个 以下作为潜在评估的子表达式(3.2)[...]:
- [...]
- 未在数学上定义的结果或不在其类型的可表示值范围内的结果;
[...]
文字常量表达式是文字类型的prvalue核心常量表达式,但不是指针类型。 [...]总的来说,文字常量表达式,引用常量表达式和地址常量表达式称为常量表达式。
因为表达式1 + 2 * INT_MIN
已经签名整数溢出,所以它不是核心常量表达式,因此不是文字常量表达式,因此不是常量表达式。因为i
的初始化不是常量表达式,所以执行动态初始化。 s
的初始化也是动态的,因为它的定义先于i
的定义,所以它的构造函数首先运行。此时,仅执行了零初始化,因此检查i == 0
应评估为真。
但是,GCC和clang同意i
可以静态初始化为1
。我和这两个人一致的经验是他们是正确的,所以我想知道......我的分析的任何部分是不正确的?
答案 0 :(得分:6)
我认为这里发生的事情是在编译时评估1 + 2 * INT_MIN
,并且在静态初始化期间初始化i
。这在[basic.start.init] / 3
允许实现使用静态存储执行非局部变量的初始化 作为静态初始化的持续时间,即使不需要静态地进行这样的初始化,也提供了 该
初始化的动态版本不会更改任何其他命名空间对象的值 初始化之前的范围,以及
初始化的静态版本在初始化变量中产生相同的值 如果所有变量都不需要静态初始化,则由动态初始化产生 动态初始化。
因此,即使在常量初始化期间不需要初始化i
,也可以在静态初始化期间初始化 ,静态初始化仍然在动态初始化之前。 s
也是如此,但我认为由于副作用,编译器无法做到这一点。
正如sftrabbit所指出的,您可以使初始化表达式任意复杂,而不是调用UB,因此i
确实只在动态初始化期间初始化。例如:
int foobar()
{
return 42;
}
int i = foobar();
在两个编译器上打印Hello, world!
。
作为旁注:为了看到1 + 2 * INT_MIN
因有符号整数溢出而调用UB,可能需要对表达式求值。这可能会在初始化期间导致UB。