I'm observing an inconsistency between GCC and Clang with respect to what is a constant evaluated context. I played with different situations:
#include <iostream>
#include <type_traits>
consteval auto ceval(auto x) { return x * x; }
constexpr auto constexprif(auto x) {
if constexpr (std::is_integral_v<decltype(x)>) {
return ceval(x);
} else {
return x + 2.;
}
}
constexpr auto constexprif_consteval(auto x) {
if constexpr (std::is_integral_v<decltype(x)>) {
if consteval {
return ceval(x);
} else {
return x * x + 1;
}
} else {
return x + 2.;
}
}
constexpr auto constevalif(auto x) {
if consteval {
return ceval(x);
} else {
return static_cast<decltype(x)>(x + 2.);
}
}
constexpr auto isconstantevaluated(auto x) {
if (std::is_constant_evaluated()) {
return ceval(x);
} else {
return static_cast<decltype(x)>(x + 2.);
}
}
int main() {
#ifdef __clang__ // GCC KO
auto a = constexprif(42);
const auto b = constexprif(42);
constexpr auto c = constexprif(42);
std::cout << "constexprif " << a << '\t' << b << '\t' << c << '\n';
#endif
auto d = constexprif_consteval(42);
const auto e = constexprif_consteval(42);
constexpr auto f = constexprif_consteval(42);
std::cout << "constexprif_consteval " << d << '\t' << e << '\t' << f
<< '\n';
auto g = constevalif(42);
const auto h = constevalif(42);
constexpr auto i = constevalif(42);
std::cout << "constevalif " << g << '\t' << h << '\t' << i << '\n';
// auto j = isconstantevaluated(42);
#ifdef __clang__ // GCC KO
const auto k = isconstantevaluated(42);
constexpr auto l = isconstantevaluated(42);
std::cout << "isconstantevaluated " << k << '\t' << l << '\n';
#endif
}
ceval is a basic consteval function, just to see in what context it can be called by a constexpr function.
constexprif is a function that (incorrectly) tried to return ceval if its argument is integral. Obviously, calling it with a runtime argument will not compile.
constexprif_consteval is a fixed version that will call cevalonly in a constant evaluate context.
constevalif calls ceval in a constant evaluate context and should have the same output as constexprif_consteval for integral arguments.
Eventually, isconstantevaluated is the same as constevalif but with a if (std::is_constant_evaluated()) instead of an if consteval.
The output is as follows.
For GCC:
constexprif_consteval 1765 1764 1764
constevalif 44 1764 1764
For Clang:
constexprif 1764 1764 1764
constexprif_consteval 1764 1764 1764
constevalif 1764 1764 1764
isconstantevaluated 1764 1764
I took note of this post about a Clang bug, but it's quite old now and I think that this question is going a bit deeper.
My expectations are the following.
I think that GCC rightfully refuses constexprif in all situations because instantiating the function for an integral type may lead to use it in a runtime context.
Clang seems overzealous to accept it in all cases, only on the basis that the argument is known at compile time.
With constexprif_consteval GCC seems to use the context (a non-const initialization) to chose the not consteval branch while Clang also goes for the full compile-time branch.
With constevalif we're observing the same behavior.
Eventually isconstantevaluated is always rejected by GCC, the if (std::is_constant_evaluated()) being a runtime test.
Thus my impression is that GCC is correct with respect to my understanding of the standard so far. Is it so? (yet, IMHO, the clang behavior seems easier to understand for me: arguments are known at compile-time? then the expression can be constant evaluated).
Before P2564 which was accepted for C++23, the situation was as follows:
ceval(x)is an invocation of an immediate function (i.e. a function markedconsteval). Every invocation of an immediate function that is not in an immediate function context, i.e. either in the body of anotherconstevalfunction or in the compound statement of aif consteval, is a so-called immediate invocation and must itself be a constant expression.As you are saying, because
xis not usable in constant expressions and its lifetime began outside ofceval(x),ceval(x)itself can never be a constant expression.As a result all of your attempts which use
if constevalshould succeed and all others should fail, as GCC is showing in your demonstration.Whether the
if constevalbranch is taken depends on whether the context is manifestly constant evaluated. That's the case for the initializations offandibecause they are markedconstexprand foreandhbecause they areconstintegral type variables initialized by constant expressions (which makes them usable in constant expressions as well).However,
dandgare not usable in constant expressions, because they are neither of the two categories mentioned above. Furthermore they are not static storage duration and therefore their initialization is not manifestly constant-evaluated. In these cases the runtime evaluation should be used.GCC in your demonstration seems to implement this correctly.
With P2564 the notion of "immediate-escalating" expressions and functions was introduced to propagate the constant evaluation of
constevalfunctions upwards.Then in the cases without
if consteval, becauseceval(x)is an immediate invocation that is not a constant expression and not in an immediate function context, it is an immediate-escalating expression.A specialization of a function template marked
constexpris also an immediate-escalating function.Then, because this immediate-escalating function contains the immediate-escalating expression (directly in its function body without any other intervening non-block scope), the function itself becomes an immediate function, i.e. it will behave as if it is marked
consteval.When the outer function itself is an immediate function, then the call
___(42)itself will be evaluated at compile-time, regardless of the context it is used in and the function body is always manifestly-constant evaluated. So, this works out here and the compile-time path is always chosen. Clang is implementing this correctly.The only issue I see is with the behavior after P2564 in the cases with
if consteval. Because the calls tocevalare in theif constevalbody, they are in an immediate function context and therefore can't be immediate-escalating expressions. Consequently I think the behavior in these cases should be unchanged from before P2564. However, Clang seems to incorrectly choose the compile-time path fordandg.