I can't fathom the essence of this error so pardon me if the title could be better. This code fails to compile:
template <auto v>
struct value_as_type {
using type = decltype(v);
static constexpr type value {v};
constexpr operator type() const {
return v;
}
};
template <int First, int Last, typename Functor>
constexpr void static_for([[maybe_unused]] Functor&& f)
{
if constexpr (First < Last)
{
f(value_as_type<First>{});
static_for<First + 1, Last, Functor>(std::forward<Functor>(f));
}
}
template <class... FieldsSequence>
struct DbRecord
{
private:
static constexpr bool checkAssertions()
{
static_assert(sizeof...(FieldsSequence) > 0);
static_for<1, sizeof...(FieldsSequence)>([](auto&& index) {
constexpr int i = index;
static_assert(i > 0 && i < sizeof...(FieldsSequence));
});
return true;
}
private:
static_assert(checkAssertions());
};
The faulting line is constexpr int i = index;
, and the error is "expression did not evaluate to a constant".
Why is this? I expect the conversion operator of the value_as_type<int>
object to be invoked. And most confusingly, it does work just fine if the lambda takes auto index
rather than auto&& index
.
Online demo: https://godbolt.org/z/TffIIn
Here's a shorter reproduction, consider the difference between a program compiled with ACCEPT
and a program without:
struct One { constexpr operator int() const { return 1; } };
template <typename T>
constexpr int foo(T&& t) {
#ifdef ACCEPT
return t;
#else
constexpr int i = t;
return i;
#endif
}
constexpr int i = foo(One{});
As my choice of macro might suggest, the ACCEPT
case is ok and the other case is ill-formed. Why? The rule in question is [expr.const]/4.12:
An expression
e
is a core constant expression unless the evaluation ofe
, following the rules of the abstract machine, would evaluate one of the following: [...] an id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either [...]
What is preceding initialization? Before I answer that, lemme provide a different program and walk through what the semantics of it have to be:
struct Int { constexpr operator int() const { return i; } int i; };
template <int> struct X { };
template <typename T>
constexpr auto foo(T&& t) {
constexpr int i = t;
return X<i>{};
}
constexpr auto i = foo(Int{1});
constexpr auto j = foo(Int{2});
There is only one function foo<Int>
, so it has to have one specific return type. If this program were allowed, then foo(Int{1})
would return an X<1>
and foo(Int{2})
would return an X<2>
-- that is, foo<Int>
can return different types? This cannot happen, so this has to be ill-formed.
When we're in a situation that requires a constant expression, think of it as opening a new box. Everything within that box must satisfy the rules of the constant evaluation as if we'd just started from that point. If we require a new constant expression nested within that box, we open a new box. Boxes all the way down.
In both the original reproduction (with One
) and the new reproduction (with Int
), we have this declaration:
constexpr int i = t;
This opens a new box. The initializer, t
, has to satisfy the restrictions of constant expressions. t
is a reference type, but has no preceding initialization within this box, hence this is ill-formed.
Now in the accepted case:
struct One { constexpr operator int() const { return 1; } };
template <typename T>
constexpr int foo(T&& t) {
return t;
}
constexpr int i = foo(One{});
We only have the one box: the initialization of the global i
. Within that box, we do still evaluate an id-expression of reference type, within that return t;
, but in this case we do have a preceding initialization within our box: we have visibility where we bind t
to One{}
. So this works. There is no contradiction that can be constructed from these rules. Indeed, this would be fine too:
constexpr int j = foo(Int{1});
constexpr int k = foo(Int{2});
static_assert(i+k == 3);
Because we still just have the one entrance each time into constant evaluation, and within that evaluation, the reference t
has preceding initialization, and the members of Int
are usable in constant expressions also.
Removing the reference works because we no longer violate the reference restriction, and there isn't any other restriction that we could violate. We're not reading any variable state or anything, the conversion function just returns a constant.
A similar example where we tried to pass Int{1}
to foo
by value would still fail - not for the reference rule this time, but instead for the lvalue-to-rvalue conversion rule. Basically, we're reading something that we can't be allowed to read -- because we'd end up with the same kind of contradiction of being able to construct a function with multiple return types.