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
ACCEPTand a program without:As my choice of macro might suggest, the
ACCEPTcase is ok and the other case is ill-formed. Why? The rule in question is [expr.const]/4.12:What is preceding initialization? Before I answer that, lemme provide a different program and walk through what the semantics of it have to be:
A contradiction
There is only one function
foo<Int>, so it has to have one specific return type. If this program were allowed, thenfoo(Int{1})would return anX<1>andfoo(Int{2})would return anX<2>-- that is,foo<Int>can return different types? This cannot happen, so this has to be ill-formed.The smallest possible box
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 (withInt), we have this declaration:This opens a new box. The initializer,
t, has to satisfy the restrictions of constant expressions.tis a reference type, but has no preceding initialization within this box, hence this is ill-formed.Now in the accepted case:
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 thatreturn t;, but in this case we do have a preceding initialization within our box: we have visibility where we bindttoOne{}. So this works. There is no contradiction that can be constructed from these rules. Indeed, this would be fine too:Because we still just have the one entrance each time into constant evaluation, and within that evaluation, the reference
thas preceding initialization, and the members ofIntare usable in constant expressions also.Back to the OP
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}tofooby 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.