Search code examples
c++constexprstatic-initializationconstinit

Static-storage variable initialized by calling constexpr function with constexpr arguments


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?


Solution

  • 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]

    [basic.start.static]/2:

    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.

    [dcl.constinit]/2:

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