Yeah, this rule is one of the more complex ones as far as constant evaluation is concerned.
Basically, you cannot have a constexpr reference to an object that doesn't have static storage duration. Taking a reference to an object is basically copying its address - and in order for an object's address to be a constant expression, the address itself needs to be constant - so it has to persist. That is, it needs to be static
.
So if you change the things you're referring to to have static storage duration instead, everything works:
static constexpr int a = 3;
static constexpr int b = 4;
constexpr Operation op(a, b); // now ok
The specific rule your program violates is [expr.const]/10, and T.C. helped me understand how it applies. Declaring a constexpr
variable requires the initialization to be a constant expression ([dcl.constexpr/10]):
In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression.
We don't say this, but it makes sense and certainly helps resolve this particular situation, but the "full-expression of the initialization" can be interpreted as a prvalue -- since a prvalue is an expression whose evaluation initializes an object ([basic.lval]/1).
Now, [expr.const]/10 reads:
A constant expression is either a glvalue core constant expression [...], or or a prvalue core constant expression whose value satisfies the following constraints:
- if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression,
- [...],
- if the value is an object of class or array type, each subobject satisfies these constraints for the value.
An entity is a permitted result of a constant expression if it is an object with static storage duration that either is not a temporary object or is a temporary object whose value satisfies the above constraints, or if it is a non-immediate function.
The initialization Operation(a, b)
is a prvalue, so we need each reference data member to refer to an entity that is permitted as a result of a constant expression. Our reference data members refer to a
and b
, neither of which has static storage duration nor are temporaries nor are non-immediate functions. Hence, the overall initialization isn't a constant expression, and is ill-formed.
Making a
and b
static gives them static storage duration, which makes them permitted results of constant expressions, which makes the prvalue initialization satisfy all the requirements, which makes the declaration of op
valid.
This is all a long winded way of saying: when dealing with constant evaluation, everything everywhere has to be constant all the way down. Some of our ways of wording this are very complex (like this one), but it's based on the fundamental idea that the model of constant evaluation is basically like pausing evaluating the code to go run a separate program to produce an answer. Producing op
requires these addresses to be known, fixed things - and that only happens for static storage duration.