🔴 🟡 🟢Understanding C++23 if consteval
← Back to Posts

Understanding C++23 if consteval

if consteval

const syntax timeline:

  • const original language
  • constexpr C++11
  • if constexpr C++17
  • consteval and constinit C++20
  • if consteval C++23 aka "consteval if", colloquially

Background & Motivation

consteval is an "immediate" function -- every call to it has to be executed during compile time. The constexpr keyword indicates the possibility of a function being run at compile-time. If the arguments to a constexpr function were all compile-time constants, the compiler could evaluate the constexpr function at compile-time, but that would only be a compiler optimization (not required by the language, except in manifestly constant-evaluated contexts that demand a constant expression (e.g. array bound). Here the compiler is required to perform evaluation at compile time).

So pre C++23, calling constexpr from consteval does work, but calling consteval functions from constexpr functions does not work. After all, constexpr could be evaluated at run time.

Using std::is_constant_evaluated() also won't work. And we don't expect it to! We use std::is_constant_evaluated() in a constexpr function to select between a compile-time-friendly implementation and a potentially-faster runtime implementation. Function parameters are not constexpr, and using std::is_constant_evaluated() within a constexpr function doesn't change the overall potential for runtime function execution. The compiler must still be able to generate a valid runtime version of the entire function with all code paths, so this may cause static analysis errors / fail to compile. These failures prevent the possibility of a consteval call ending up in runtime code.

  • all code examples are currently directly from Sandor Dargo's blog! See Sources.
consteval int bar(int i) {
    return 2*i;
}

constexpr int foo(int i) {
    if (std::is_constant_evaluated()) {
        return bar(i);
    }
    return 2*i;
}

int main() {
  [[maybe_unused]] auto a = foo(5);
}
/*
main.cpp: In function 'constexpr int foo(int)':
main.cpp:6:14: error: 'is_constant_evaluated' is not a member of 'std'
    6 |     if (std::is_constant_evaluated()) {
      |              ^~~~~~~~~~~~~~~~~~~~~
main.cpp:7:19: error: 'i' is not a constant expression
    7 |         return bar(i);
      |                ~~~^~~

*/

Introducing C++23 if consteval!

Enter if consteval in C++23! Now we can call consteval functions from constexpr ones. The syntax looks exactly as we had wanted if (std::is_constant_evaluated) to work, but we can invoke consteval functions if the if condition evaluates to true, aka when the context is constant-evaluated, but not necessarily explicitly declared so. We continue to safely prevent consteval calls ending up in runtime code! (Also, you don't need any header include)

consteval int bar(int i) {
    return 2*i;
}

int foo(int i) {
    if consteval {
        return bar(i);
    }
    return 2*i;
}

int main() {
  [[maybe_unused]] auto a = foo(5);
}

How implemented

#TODO

Takeaways

The benefits from unlocking "call-consteval-from-constexpr" are in line with all motivations for expanding compile-time capabilities:

  • We can implement different optimizations for compile time vs. runtime. if consteval allows the compiler to choose between a slow but simple-to-implement constexpr compile-time version, or a highly optimized (maybe assembly-level) runtime version.
  • We can access compile-time library features (e.g. std::source_location::current()) when available during constant evaluation, while providing a fallback (or simply doing nothing) at runtime.
  • We can use if consteval to wrap compile-time assertions (static_assert) or other static analysis code that should only be checked during compilation.
  • In certain scenarios, we can generate and populate data structures or lookup tables entirely at compile time, avoiding runtime overhead.

We should point out the difference between C++23 if consteval and C++17 if constexpr. if constexpr is a compile-time conditional that causes the compiler to discard branches of code at compilation based on a constexpr. This differs from a regular if statement, where both branches are compiled, and the condition is evaluated at runtime. if constexpr is primarily used in generic template programming and largely supplants SFINAE/manual template specialization, where different code paths may be required based on the properties of a template type (e.g., using type traits like std::is_integral_v). And in fact C++26 reflection in concert with if constexpr vastly expands our compile-time and metaprogramming toolboxes.

Sources (some)

https://www.sandordargo.com/blog/2022/06/01/cpp23-if-consteval