c++referencetype-conversionlanguage-lawyerconstexpr

Error when binding a constexpr reference to variable of different type


It is known in C++ that when initializing a reference, types must match. So the following code snippet causes an error (in global scope, the same below):

unsigned i; 
int & p = i;  // error

But there is an exception for reference to const, so the following is correct where a type conversion is allowed:

unsigned i; 
const int & p = i;  // ok

But if I define p as constexpr, a compiling error occurs:

unsigned i; 
constexpr const int & p = i;  // error: the value of 'i' is not usable in a constant expression

The error message says that i is not a const, which I think is irrelevant, because if I change the type from signed to unsigned, i.e., no type conversion, there is no compiling error:

unsigned i; 
constexpr const unsigned & p = i;  // ok

const can be omitted here.

So, it seems that it is the type conversion that caused the compiling error in

unsigned i; 
constexpr const int & p = i;  // error

But I am not sure. Can you please let me know what C++ standard rule caused this error? I tested the above codes in both GCC and MSVC, under C++20.


Solution

  • Let's focus on the "broken" snippet step by step:

    unsigned i; 
    constexpr const int& p = i;
    

    The rules for using constexpr specifier in an object declaration are covered by dcl.constexpr#6:

    A constexpr specifier used in an object declaration declares the object as const. Such an object shall have literal type and shall be initialized. A constexpr variable shall be constant-initializable ([expr.const]). A constexpr variable that is an object, as well as any temporary to which a constexpr reference is bound, shall have constant destruction.

    A variable v is constant-initializable if it meets criteria of expr.const#6:

    • the full-expression of its initialization is a constant expression when interpreted as a constant-expression, and
    • immediately after the initializing declaration of v, the object or reference x declared by v is constexpr-representable, and
    • if x has static or thread storage duration, x is constexpr-representable at the nearest point whose immediate scope is a namespace scope that follows the initializing declaration of v.

    So in the end of the day, it all boils down to whether full-expression of initialization of p is a constant expression. expr.const#22 gives the following definition (the prvalue constraints are not relevant here, so I omitted them):

    A constant expression is either a glvalue core constant expression that refers to an object or a non-immediate function, or a prvalue core constant expression whose value satisfies the following constraints:

    ...

    Essentially i is a glvalue, however when binding a reference of unrelated type (const int& to unsigned int) you end up with so-called lvalue-to-rvalue conversion, which does not qualify as core constant expression as per expr.const#10.9:

    An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:

    ...

    • an lvalue-to-rvalue conversion unless it is applied to

      • a glvalue of type cv std​::​nullptr_t,
      • a non-volatile glvalue that refers to an object that is usable in constant expressions, or
      • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;

    So your options here are either avoid this conversion, by changing the reference type and binding directly:

    unsigned i; 
    constexpr const unsigned& p = i;
    

    or by making i "usable in constant expressions":

    const unsigned i = 0;
    constexpr const int& p = i;