I have the following infinitely recursive constexpr
function:
constexpr int foo(int x) {
return foo(x + 1);
}
Then I found that
int main() {
static int x = foo(5);
static int y = std::integral_constant<int, foo(5)>::value;
static constinit int z = foo(5);
}
the compiler (GCC13, on Ubuntu 22.04) reports error on the initialization of y
and z
due to compile-time infinite recursion, but no error for x
. If we remove the declarations for y
and z
and then run the program, Address Boundary Error is caused.
Here 5
is a constant expression, but the initialization of x
with foo(5)
is not performed in compile-time. Why?
What's more, I tried to modify foo
:
constexpr int foo(int x) {
if (std::is_constant_evaluated())
return 42;
return foo(x + 1);
}
Then I found that x
is initialized to 42, with no compile-time or run-time infinite recursions, which indicates that it happens at compile-time this time.
So, is the initializer foo(5)
for x
evaluated at compile-time? Is this initialization static initialization or dynamic initialization? What are the actual rules about that?
Moreover, I'm not so familiar with constinit
. The reason for initializing y
with std::integral_constant
is that we want to ensure static initialization. Can I say that writing constinit
is always a safe and modern alternative to it?
The static int
variable x
is constant-initialized if and only if its initializer (foo(5)
) is a constant expression, and foo(5)
is a constant expression only if it doesn't have infinite recursion (for obvious reason). If x
is not constant-initialized, then dynamic initialization is performed.
[expr.const]/2, emphasis mine:
A variable or temporary object o is constant-initialized if
- either it has an initializer or its default-initialization results in some initialization being performed, and
- the full-expression of its initialization is a constant expression when interpreted as a constant-expression, except that if o is an object, that full-expression may also invoke constexpr constructors for o and its subobjects even if those objects are of non-literal class types.
[Note 2: Such a class can have a non-trivial destructor. Within this evaluation, std::is_constant_evaluated() ([meta.const.eval]) returns true. — end note]
Constant initialization is performed if a variable or temporary object with static or thread storage duration is constant-initialized ([expr.const]). [...] Together, zero-initialization and constant initialization are called static initialization; all other initialization is dynamic initialization.
And constinit
is the right way to ensure static initialization.
If a variable declared with the
constinit
specifier has dynamic initialization ([basic.start.dynamic]), the program is ill-formed, even if the implementation would perform that initialization as a static initialization ([basic.start.static]).