C++ / TEMPLATES AND GENERIC PROGRAMMING
consteval, constinit, and compile-time guarantees
Use consteval and constinit to turn compile-time evaluation from a permission the compiler may quietly ignore into a checked requirement.
What you will learn
- Pick between constexpr, consteval and constinit by the guarantee each one checks
- Report compile-time validation failures by throwing inside a consteval body
- Use constinit to remove dynamic initialisation without making a variable const
- Predict when a consteval call is rejected inside constexpr or templated code
Understanding consteval, constinit, and compile-time guarantees
constexpr on a function is permission, not obligation: if the arguments are not constant expressions, or the call simply appears where no constant is required, the function compiles into ordinary machine code and nothing tells you. consteval removes that freedom. Every call to a consteval function is an immediate invocation: its arguments must themselves be constant expressions, and the call is replaced by its computed result during translation. The consequence is that a consteval function has no run-time existence for callers, so you cannot pass it as a callback or feed it a value read from a file, and the failure is reported by the compiler instead of by a profiler.
constinit governs the other half of the story, namely when a variable with static or thread storage duration receives its value. Such a variable is either constant-initialised, meaning the value sits in the object file and no code runs before main, or dynamically initialised, in which case its order relative to other translation units is unspecified and reading it early gives you a zero. constexpr guarantees constant initialisation but also forces the object to be const; constinit asserts exactly the same guarantee for a variable you intend to modify later. Because it is an assertion, a later edit that reintroduces dynamic initialisation becomes a compile error rather than a startup-order bug.
The mental model is three separate promises: constexpr says this may be evaluated during translation, consteval says this call must be, constinit says this variable's initialisation must be. Since the last two are checked, they do not compose freely. In C++20 a plain constexpr function whose body calls a consteval function with its own parameters is ill-formed even if it is only ever used in constant expressions, because parameters are never constants inside the body. C++23 relaxes this for templated functions and lambdas by letting them become immediate functions implicitly, which means adding one consteval call inside a function template can silently take away that template's ability to run at run time.
<iostream>
consteval int ipow(int base, int exp) { // immediate function
int r = 1;
while (exp-- > 0) r *= base;
return r;
}
constexpr int ipow_maybe(int base, int exp) { // permitted, not required
int r = 1;
while (exp-- > 0) r *= base;
return r;
}
int runtime_exp() { return 3; } // ordinary function
// constinit int oops = runtime_exp(); // error: no constant initialiser
constinit int bucket_count = ipow(2, 10); // value is in the image already
int main() {
static_assert(ipow(3, 4) == 81);
std::cout << bucket_count << '\n';
std::cout << ipow(2, 16) << '\n'; // folded to 65536
std::cout << ipow_maybe(2, runtime_exp()) << '\n'; // silently runs at run time
// std::cout << ipow(2, runtime_exp()); // error: not a constant expression
bucket_count += 1; // constinit does not imply const
std::cout << bucket_count << '\n';
}constexpr grants permission to evaluate during translation, while consteval and constinit turn that permission into a requirement the compiler enforces, one on every call and one on a variable's initialisation.
Worked examples
Validating a literal at translation time
A consteval parser that rejects bad input by throwing, feeding both a constinit variable and a template argument.
<iostream>
<string_view>
consteval unsigned parse_hex(std::string_view s) {
unsigned v = 0;
for (char c : s) {
v <<= 4;
if (c >= '0' && c <= '9') v |= unsigned(c - '0');
else if (c >= 'a' && c <= 'f') v |= unsigned(c - 'a' + 10);
else throw "hex digit expected";
}
return v;
}
template <unsigned Colour>
struct Swatch {
static void print() { std::cout << Colour << '\n'; }
};
constinit unsigned background = parse_hex("ff8000");
int main() {
std::cout << background << '\n';
Swatch<parse_hex("00ff00")>::print();
// std::cout << parse_hex("00gg00"); // error: throw during evaluation
}Example explained
Line 1The throw is unreachable at run time; if constant evaluation reaches it the compiler reports the call site as not a constant expression, which is how compile-time validation delivers its message.
Line 2constinit unsigned background records 0xff8000 directly in the data section, so no initialiser code runs before main.
Line 3Swatch<parse_hex("00ff00")> proves the call really produced a constant: a template argument admits nothing else.
The context decides, unless you use consteval
Shows that std::is_constant_evaluated() depends on where a constexpr function is called from, while a consteval function has only one path.
<iostream>
<type_traits>
constexpr int tag(int x) {
if (std::is_constant_evaluated()) return x + 1000; // translation-time path
return x; // run-time path
}
consteval int always_tag(int x) {
if (std::is_constant_evaluated()) return x + 1000;
return x; // never reachable
}
int main() {
constexpr int a = tag(1); // forced constant evaluation
int r = 1;
std::cout << a << '\n';
std::cout << tag(r) << '\n'; // run-time argument
std::cout << tag(1) << '\n'; // literal argument, run-time context
std::cout << always_tag(1) << '\n'; // immediate invocation
}Example explained
Line 1constexpr int a = tag(1); is a manifestly constant-evaluated initialiser, so is_constant_evaluated() is true and a becomes 1001.
Line 2tag(1) as a stream argument is an ordinary run-time call even though the argument is a literal: the calling context decides, not the argument.
Line 3always_tag(1) must be a constant expression, so the compile-time branch is the only one taken and 1001 is written into the call site.
Line 4The return x; inside always_tag can never execute, which is the point: consteval collapses the two paths into one.
Important notes
constinit is permitted only on variables with static or thread storage duration and may not be combined with constexpr; constinit const is legal and is what you want when the value is also needed in constant expressions.
A consteval function's parameters are not constant expressions inside its body, so static_assert(exp >= 0) there does not compile; test with a plain if and throw instead.
Common mistakes
Writing if constexpr (std::is_constant_evaluated()): the condition of an if constexpr is itself manifestly constant-evaluated, so it is always true, the run-time branch is discarded, and the compile-time path runs everywhere, often much slower.
Reading constinit as const: constinit int n = 4; still allows n = 5;, and n cannot be used as a template argument or inside static_assert because it is not usable in constant expressions.
Deleting consteval to silence an error about calling it from a constexpr helper: that makes the code compile while losing the guarantee at every call site, instead of making the helper consteval too or passing the value as a template parameter.
Try it yourself
Change, predict, then run
Write consteval unsigned checksum(std::string_view s) that sums the character codes and throws when s is empty, static_assert two known results, then initialise constinit unsigned id = checksum("kiro"); and print it. Now pass a string_view built from a non-const local variable and read the error the compiler gives you.
Open the C++ workspaceCheck your understanding
A constexpr function hash(std::string_view) builds identifiers, and profiling shows some calls executing at run time. Which single change makes the compiler reject every call that would not be evaluated during translation?
- Change hash's declaration from constexpr to consteval
- Declare every variable that stores a hash as constinit
- Wrap hash's body in if constexpr (std::is_constant_evaluated())
- Add const to every variable that stores a hash
Show answer
consteval makes each call an immediate invocation whose arguments must be constant expressions, so a call fed run-time data becomes a compile error rather than emitted code. constinit is the tempting answer but it only constrains how static and thread-duration variables are initialised and says nothing about hash calls elsewhere; const on a local merely allows the compiler to fold the call, it does not require it. The if constexpr option is actively harmful, since that condition is manifestly constant-evaluated and therefore always true.