Test an expression for constexpr-friendliness
Via Mark de Wever on the cpplang Slack: Mark demonstrates how to tell whether a snippet of code is constexpr-friendly or not. (Godbolt.)
template<class F, int = (F{}(), 0)>
constexpr bool is_constexpr_friendly(F) { return true; }
constexpr bool is_constexpr_friendly(...) { return false; }
int g = 42;
constexpr int test(int x) {
return (x == 42) ? g : x;
}
static_assert(!is_constexpr_friendly([]{ test(42); }));
static_assert(is_constexpr_friendly([]{ test(43); }));
Notice that this technique relies on default-constructing
(at compile time) an instance of the stateless lambda type F
;
the ability to default-construct stateless lambda types is new
in C++20.
In the code above, test(42)
is not a constant expression.
So (F{}(), 0)
is not a constant expression. So it can’t be
used as a template argument. So substitution of F
into
is_constexpr_friendly
#1 fails; substitution failure is not
an error; is_constexpr_friendly
#2 is chosen by overload
resolution.
C++20 also introduces consteval
functions — which are like
constexpr
functions but must produce compile-time results —
and Mark found that the above technique failed when test
was consteval
.
template<class F, int = (F{}(), 0)>
constexpr bool is_constexpr_friendly(F) { return true; }
constexpr bool is_constexpr_friendly(...) { return false; }
int g = 42;
consteval int test2(int x) {
return (x == 42) ? g : x;
}
static_assert(!is_constexpr_friendly([]{ test2(42); }));
static_assert(is_constexpr_friendly([]{ test2(43); }));
All three major vendors error out here (Godbolt). MSVC’s error message is representative:
error C7595: 'test2': call to immediate function is not a constant expression
note: failure was caused by non-constant arguments or reference to a non-constant symbol
note: see usage of 'g'
In the code above, test2(42)
is an invocation of a consteval
function, but it’s not a constant expression. That’s a hard error.
The compiler’s chain of logic stops there; we never get as far as
overload resolution.
Notice that test2
remains a valid consteval
function definition!
It’s simply ill-formed to pass it 42
.
Johel Ernesto Guerrero Peña provides an alternative idiom.
He puts the questionably-constant expression in an explicitly provided
template argument instead of a defaulted template argument,
and uses requires
to create the SFINAE context.
(Godbolt.)
template<int> struct A {};
template<class F>
constexpr bool is_constexpr_friendly(F) {
return requires {
typename A<(F{}(), 1)>;
};
}
static_assert(!is_constexpr_friendly([]{ test(42); }));
static_assert(is_constexpr_friendly([]{ test(43); }));
However, again, this fails to work for consteval
functions
like test2
.
I’m not aware of any technique to “safely simulate” the evaluation
of a consteval
function and bail out in a SFINAE-friendly manner
if it hits a problem. My impression is that this is intentional:
consteval
functions are designed as a kind of opaque box where
errors are errors and the compiler needn’t do anything “tentatively.”
If you wanted constexpr
’s fall-back-to-runtime behavior, you would
just use constexpr
; if you’re using consteval
, it’s because
you want hard errors when your consteval
function is misused.
However, if you think you know a trick to tentatively evaluate
a consteval
function, I’ll be interested to hear about it!
Sidenote 1: consteval
functions are like constexpr
variables
I noticed that the difference between constexpr
and consteval
functions is basically the same as the difference between const
and constexpr
variable templates (Godbolt):
int g = 42;
constexpr int test(int x)
{ return x == 42 ? g : x; }
consteval int test2(int x)
{ return x == 42 ? g : x; }
is analogous to
template<int X>
const int vtest = (X == 42) ? g : X;
template<int X>
constexpr int vtest2 = (X == 42) ? g : X;
The expressions test(43)
and vtest<43>
are compile-time constant
expressions. The expressions test(42)
and vtest<42>
are legal C++,
both equal to 42
at runtime, but not compile-time constants.
The expressions test2(43)
and vtest2<43>
, likewise, are compile-time
constants. But test2(42)
is a hard error: a consteval
function must not
return a runtime result. And vtest2<42>
is a hard error: a constexpr
variable must not be initialized with a runtime value.
Sidenote 2: Implementation divergence on template default arguments
Naturally, I tried to mechanically transform Mark’s code using constexpr
and consteval
functions, into the corresponding code using const
and
constexpr
variables. But I messed it up — twice! First I wrote this:
int g = 42;
#ifndef CONSTEXPR
template<int X>
const int vtest = (X == 42) ? g : X;
#else
template<int X>
constexpr int vtest2 = (X == 42) ? g : X;
#endif
template<class F, int = vtest<42>>
constexpr bool is_constexpr_friendly(F&&) { return true; }
constexpr bool is_constexpr_friendly(...) { return false; }
static_assert(!is_constexpr_friendly());
I was surprised to see three different behaviors from the three
major compiler vendors:
MSVC accepts this program with or without -DCONSTEXPR
.
GCC rejects it only with -DCONSTEXPR
. Clang rejects it unconditionally.
(Godbolt.)
Clang’s error message tells me my first stupid mistake:
error: use of undeclared identifier 'vtest'
template<class F, int = vtest<42>>
^
Oh, right. I defined vtest
in the first branch of the #ifdef
but
vtest2
in the second branch. That was silly. Let’s fix that.
int g = 42;
#ifndef CONSTEXPR
template<int X>
const int vtest = (X == 42) ? g : X;
#else
template<int X>
constexpr int vtest = (X == 42) ? g : X; // !!
#endif
template<class F, int = vtest<42>>
constexpr bool is_constexpr_friendly(F&&) { return true; }
constexpr bool is_constexpr_friendly(...) { return false; }
static_assert(!is_constexpr_friendly());
The three major vendors still have three different behaviors,
but they’ve switched places! Now, MSVC rejects the program only
with -DCONSTEXPR
; GCC unconditionally accepts; and Clang continues
to reject unconditionally.
(Godbolt.)
Anyway, this code is still messed up, and not doing what I intended
at all… because there’s no way overload resolution will ever pick
is_constexpr_friendly
#1! It takes an argument of type F&&
, but
the caller isn’t passing any arguments at all. Silly mistake!
The implementation divergence above can be reduced to this
(Godbolt):
template<int = vtest<42>>
struct A {};
It appears that GCC waits until A<int>
is actually used before
evaluating the default argument at all; Clang eagerly requires
the default argument vtest<42>
to be a constant expression even
if it’s never used; and MSVC eagerly instantiates vtest<42>
but
doesn’t require it to have a constant value if it’s never used.
MSVC’s behavior seems friendliest in this case, but from a compiler
dev’s point of view it’s arguably the least explicable: Why bother
to eagerly instantiate vtest<42>
at all, if you’re not planning
to check that it is a constant expression of the appropriate type?