C++20 consteval: Forcing Compile-Time Evaluation Where constexpr Only Allows It
Key takeaways
A consteval function must produce a constant at every call site, so passing it a runtime value is a compile error. Keep the logic in a constexpr function, put consteval at the API edge (literals, checked constructors, tables), and use if consteval (C++23) when a constexpr function needs to call it.
constexpr on a function means the function can run at compile time. Whether it actually does depends on each call. consteval, added in C++20, removes that choice: every call to a consteval function (an immediate function) must be evaluated during compilation, and a call that can’t be is a hard error. That makes it useful for rejecting bad input at build time and for things like format-string checking. It also has one sharp edge, around constexpr wrappers, that catches almost everyone.
Every example below was compiled with g++ 10.3 (-std=c++20 -Wall -Wextra). The diagnostics quoted are from that compiler unless stated otherwise. Features that need a newer compiler are labelled.
The difference in one example
constexpr int sq_ce(int x) { return x * x; }
consteval int sq_cv(int x) { return x * x; }
int main() {
int a = 5;
constexpr int k = sq_cv(5); // OK: compile time
int r = sq_cv(5); // OK: still evaluated at compile time; r is just initialized with 25
int y = sq_ce(a); // OK: constexpr function, runtime call
int z = sq_cv(a); // error
}
error: the value of 'a' is not usable in a constant expression
note: 'int a' is not const
Clang says call to consteval function 'sq_cv' is not a constant expression, and MSVC reports C7595. All three compilers mean the same thing: the arguments of an immediate call have to be constants where the call is written.
Note the int r = sq_cv(5); line. The result doesn’t have to go into a constexpr variable. Any call whose arguments are constants is fine in ordinary runtime code, and the compiler replaces it with the value.
constexpr function | consteval function | |
|---|---|---|
| Runtime call allowed | Yes | No |
| Error if a call can’t be constant | Only where a constant is required | At every call site |
| Can take its address normally | Yes | Only inside another immediate context |
| Appears in the binary | Usually, if called at runtime | Never as a callable function |
| Applies to variables | Yes (constexpr int x) | No (use constexpr or constinit) |
The trap: constexpr wrappers
consteval int square(int x) { return x * x; }
constexpr int twice_square(int x) {
return 2 * square(x); // error: 'x' is not a constant expression
}
Many articles say “a constexpr function can’t call a consteval function”. That’s not quite the rule. This version compiles:
consteval int answer() { return 42; }
constexpr int wrapper() { return answer(); } // fine: answer() is a constant by itself
The real rule: each call to an immediate function must be a constant expression by itself, unless it sits in an immediate function context (the body of another consteval function, or, from C++23, an if consteval branch). A constexpr function’s parameter isn’t a constant inside that function, because the function might run at runtime. So square(x) fails and answer() doesn’t.
The obvious workaround does not work:
constexpr int f(int x) {
if (std::is_constant_evaluated()) return square(x); // g++ 10: 'x' is not a constant expression
return x * x;
}
std::is_constant_evaluated() is an ordinary runtime query. The immediate call inside the branch is still checked on its own. Here are the three real fixes:
- Make the wrapper consteval too, if it never needs to run at runtime.
- C++23
if consteval(GCC 12+, Clang 14+). Its first branch is an immediate function context, soif consteval { return square(x); } else { return x * x; }is valid. I couldn’t compile this with g++ 10.3. - C++23 consteval propagation (P2564), adopted as a defect report and implemented in GCC 14 and Clang 17. A constexpr function (or function template specialization) that contains such a call is promoted to consteval automatically, instead of the code being rejected. On an older compiler the same code is an error, so code relying on this doesn’t build on older toolchains.
In practice, whenever I switched a hashing or parsing helper from constexpr to consteval, it spread up the call graph: every generic helper that forwarded the argument had to become consteval as well, until the change hit a function that also had runtime callers and could go no further. That’s why the pattern below, a constexpr core with a consteval entry point, is the one that holds up.
Pattern: constexpr core, consteval front door
The most common consteval mistake in tutorials is making a hash function consteval and then calling it on a runtime string in switch:
consteval unsigned djb2(const char* s);
void handle(const std::string& t) {
switch (djb2(t.c_str())) { /* ... */ } // error: call to non-'constexpr' function '... c_str() const'
}
The case labels need a compile-time hash, but the switch value is a runtime string. Write the algorithm once as constexpr. Then add a consteval wrapper for the places that must be compile time. Here that’s a user-defined literal, so a non-constant use can’t slip in:
#include <cstdint>
#include <cstdio>
#include <string_view>
constexpr std::uint32_t fnv1a(std::string_view s) {
std::uint32_t h = 2166136261u;
for (unsigned char c : s) { h ^= c; h *= 16777619u; }
return h;
}
consteval std::uint32_t operator""_id(const char* s, std::size_t n) {
return fnv1a({s, n});
}
void dispatch(std::string_view cmd) {
switch (fnv1a(cmd)) { // runtime: constexpr function
case "login"_id: std::puts("login"); break;
case "logout"_id: std::puts("logout"); break;
default: std::puts("unknown");
}
}
dispatch("logout") prints logout, and an unknown command falls through to default. A hash switch still has to deal with collisions. If two case labels collide, the compiler rejects the duplicate case value, but a runtime string that happens to collide with a label needs a string comparison after the match if that matters to you.
Pattern: consteval constructors validate literals
This is the pattern the standard library itself relies on. The constructor of std::basic_format_string is consteval, and that’s how std::format("{:d}", "text") becomes a compile error instead of a runtime exception. The mechanism is simple: a throw that is actually reached during constant evaluation makes the evaluation fail.
class Port {
public:
consteval Port(int p) : v_(p) {
if (p < 1 || p > 65535) throw "port out of range";
}
constexpr int value() const { return v_; }
private:
int v_;
};
void listen(Port p);
listen(8080); // OK
listen(70000); // error
in 'constexpr' expansion of 'Port(70000)'
error: expression '<throw-expression>' is not a constant expression
| if (p < 1 || p > 65535) throw "port out of range";
The error points at the throw line, so the thrown string works as the error message a user reads. On the valid path the throw is never reached, so it costs nothing. The trade-off is that Port can’t be built from a runtime value. If you need that too, add a separate named factory (Port::checked(int)) that validates at runtime. Don’t give the consteval constructor a constexpr overload: overloads can’t differ only in consteval-ness.
std::source_location::current() is also consteval. As a default argument it’s evaluated at each caller’s site, which is why void log(std::source_location loc = std::source_location::current()) reports the caller’s line.
Pattern: tables built at compile time, used at runtime
A consteval function can build data. A constexpr variable then stores the result, and runtime code reads it:
#include <array>
#include <cstdint>
#include <string_view>
consteval std::array<std::uint32_t, 256> make_crc_table() {
std::array<std::uint32_t, 256> t{};
for (std::uint32_t i = 0; i < 256; ++i) {
std::uint32_t c = i;
for (int k = 0; k < 8; ++k) c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1;
t[i] = c;
}
return t;
}
constexpr auto crc_table = make_crc_table();
std::uint32_t crc32(std::string_view data) {
std::uint32_t c = 0xFFFFFFFFu;
for (unsigned char b : data) c = crc_table[(c ^ b) & 0xFF] ^ (c >> 8);
return c ^ 0xFFFFFFFFu;
}
// crc32("123456789") == 0xCBF43926, the standard CRC-32 check value
Use unsigned types here. Versions of this example that use int hold 0xEDB88320 in a signed value, and depending on the compiler and standard the shifts give the wrong table or overflow.
Pattern: predicates in constraints
consteval bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; ++i) if (n % i == 0) return false;
return true;
}
template <int N> requires (is_prime(N))
struct PrimeTable { std::array<int, N> slots{}; };
PrimeTable<17> ok;
PrimeTable<16> bad; // error: template constraint failure ... evaluated to 'false'
A constexpr function would do exactly the same job here, because a requires clause is a constant context anyway. Use consteval only when you want to forbid runtime use of the predicate.
What a consteval body can and cannot do
- Allowed: loops, local variables, recursion (within the compiler’s limits),
std::array,std::string_view, and since C++20 transient dynamic allocation.new int[n]followed bydelete[]in the same evaluation compiles with g++ 10.constexpr std::vector/std::stringneed library support (libstdc++ 12, recent libc++ and MSVC STL). - Not allowed: calling non-constexpr functions.
std::isalphafails with'isalpha(97)' is not a constant expression, so write your ownc >= 'a' && c <= 'z'. Also not allowed: I/O,reinterpret_cast, reading non-constant globals, undefined behavior (signed overflow, out-of-bounds access), and memory still allocated when evaluation ends. - Address-taking:
auto p = □in normal code fails withtaking address of an immediate function 'consteval int square(int)'. A pointer to an immediate function can’t escape to runtime. - Lambdas:
[](int x) consteval { return x * x; }is valid C++20, and the same rules apply to each call.
Compile-time cost
consteval doesn’t make evaluation slower or faster than constexpr. The same constant evaluator runs either way. The costs to watch are the ones of any constant evaluation: very deep recursion (GCC’s -fconstexpr-depth, default 512), long loops (-fconstexpr-loop-limit / -fconstexpr-ops-limit), and large generated tables that get re-evaluated in every translation unit that includes the header. When a big table slows the build, move it to a single .cpp file, or declare it inline constexpr in a header so it’s defined once instead of copied into every translation unit. If you’re not sure, measure with Clang’s -ftime-trace.
Compiler support
- GCC 10+ supports consteval. All examples here compile with 10.3.
if constevalneeds GCC 12, and P2564 propagation needs GCC 14. - Clang 11+ accepts consteval, but some corner cases (default arguments, nested immediate calls) were only fixed in later releases, up to Clang 17, which also implements P2564.
if constevalneeds Clang 14. - MSVC: VS 2019 16.10 (19.29) and later.
The feature-test macro is __cpp_consteval (201811L for C++20, 202211L once P2564 is implemented). Checking for the propagation value is a better guide than checking compiler versions if you ship code that relies on it.
When to use which
- Callers need both compile-time and runtime → constexpr.
- The result must be known at build time, and a runtime call is a bug → consteval. Examples: literal validators, format-string checkers, ID literals,
source_location-style capture. - A variable must be constant-initialized but may change later → constinit. For a variable that must be a compile-time constant → constexpr. consteval doesn’t apply to variables.
- Generic library code that forwards arguments → keep it constexpr. Add consteval at the outermost user-facing API, so the requirement doesn’t spread to every helper.