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.
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 asconst
. 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 referencex
declared byv
isconstexpr
-representable, and- if
x
has static or thread storage duration,x
isconstexpr
-representable at the nearest point whose immediate scope is a namespace scope that follows the initializing declaration ofv
.
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 ofE
, 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;