评估潜在常量表达式期间的未定义行为

Undefined behaviour during evaluation of a potential constant expression

本文关键字:未定义 常量 表达式 评估      更新时间:2023-10-16

考虑这个程序:

#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。当这两个人达成一致时,我的经验是他们是正确的,所以我想知道。。。我的分析有什么地方不正确吗?

我认为这里发生的事情是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。