我有以下无限递归
constexpr
函数:
constexpr int foo(int x) {
return foo(x + 1);
}
然后我发现
int main() {
static int x = foo(5);
static int y = std::integral_constant<int, foo(5)>::value;
static constinit int z = foo(5);
}
由于编译时无限递归,编译器(GCC13,在 Ubuntu 22.04 上)报告
y
和 z
的初始化错误,但 x
没有错误。如果我们删除y
和z
的声明然后运行程序,就会导致地址边界错误。
这里
5
是一个常量表达式,但是 x
和 foo(5)
的初始化不是在编译时执行的。为什么?
更重要的是,我尝试修改
foo
:
constexpr int foo(int x) {
if (std::is_constant_evaluated())
return 42;
return foo(x + 1);
}
然后我发现
x
被初始化为42,没有编译时或运行时无限递归,说明这次是在编译时发生的。
那么,
foo(5)
的初始化器x
是否在编译时评估?这个初始化是静态初始化还是动态初始化?实际规则是什么?
而且,我也不太熟悉
constinit
。使用 y
初始化 std::integral_constant
的原因是我们要确保静态初始化。我可以说写作constinit
始终是一种安全且现代的替代方案吗?
当且仅当
static int
变量 x
的初始值设定项 (foo(5)
) 是常量表达式时,它才会被常量初始化。如果不是常量初始化,则进行动态初始化。
[expr.const]/2,强调我的:
变量或临时对象 o 是 常量初始化 if
- 它有一个初始化程序或其默认初始化会导致执行一些初始化,并且
- 当解释为常量表达式时,其初始化的完整表达式是常量表达式,但如果 o 是一个对象,则该完整表达式也可以调用 o 及其子对象的 constexpr 构造函数,即使这些对象是非文字类类型。
[注2: 这样的类可以有一个不平凡的析构函数。在此评估中,std::is_constant_evaluated() ([meta.const.eval]) 返回 true。 — 尾注]
如果具有静态或线程存储持续时间的变量或临时对象被常量初始化([expr.const]),则执行常量初始化。 [...] 零初始化和常量初始化一起称为“静态初始化”;所有其他初始化都是动态初始化。
并且
constinit
是确保静态初始化的正确方法。
[dcl.constinit]/2如果使用constinit
说明符声明的变量具有动态初始化 ([basic.start.dynamic]),则程序的格式不正确,即使实现将该初始化作为静态初始化执行 ([basic.start.static] ]).