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
constinitspecifier 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]).