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?

Posted 2022-01-04