Search code examples
c++language-lawyerconstexprconstant-expressionconstinit

Why does constinit allow UB?


Say I initialize variables like this:

#include <cstdint>

constexpr uint16_t a = 65535;
constinit int64_t b = a * a; // warning: integer overflow in expression of type 'int' results in '-131071' [-Woverflow]
constexpr int64_t c = a * a; // error: overflow in constant expression [-fpermissive]

Both b and c produce undefined behavior because of integer overflow.

With constinit the variable is constant initialized. Which makes no guarantee about UB.

With constexpr the variable is initialized with a constant expression. Constant expression guarantee not to have any UB. So here the signed integer overflow in an error. But the variable is also automatically const.

So how do I best initialize a non-const variable with a constant expression?

Do I have to write

constexpr int64_t t = a * a; // error: overflow in constant expression [-fpermissive]
constinit int64_t b = t;

or

constinit int64_t b = []()consteval{ return a * a; }(); // error: overflow in constant expression

every time?


Solution

  • This is related to CWG issue 2543.

    As it stands currently, because the compiler is allowed to replace any dynamic initialization with static initialization if it can and because constinit is only specified to enforce "no dynamic initialization", it might still allow an initializer which is not a constant expression (maybe dependent on the interpretation as discussed in the linked issue). constinit therefore reflects whether there will actually be initialization at runtime (which is relevant to avoiding dynamic initialization order issues). It does not necessarily reflect whether the initializer is a constant expression.

    As stated in the issue description, this is practically not really implementable though because the dynamic/static initialization choice is made too late in the compilation process to always make constinit reflect it properly.

    With one possible resolution of the issue, the specification of constinit might be changed to actually require the variable to be constant-initialized instead of just requiring that there is no dynamic initialization. If that was the resolution taken, then your first example for the initialization of b would also require the compiler to diagnose the UB and all of the other solutions would become obsolete.

    The issue description doesn't seem to really favor any direction though.


    For the current situation (and if the resolution is taken in another direction), an alternative to the solutions you gave is:

    template<typename T>
    consteval auto force_compiletime(T&& t) {
        return std::forward<T>(t);
    }
    

    or

    template<typename To, typename T>
    consteval To force_compiletime2(T&& t) {
        return std::forward<T>(t);
    }
    

    and then

    constinit auto t = force_compiletime(static_cast<int64_t>(a * a));
    

    or

    constinit auto t = force_compiletime2<int64_t>(a * a);
    

    Note that you need to include the target type in this way in the initializer, otherwise any potentially UB in the conversion will not be diagnosed. If you don't care about that

    constinit int64_t t = force_compiletime(a * a);
    

    would also be fine.


    Technically the solution with the consteval lambda from your question is ill-formed, no diagnostic required, because the lambda is marked consteval but can never produce a constant expression when called. But I would expect any non-malicious compiler to still diagnose such a call.