constexpr, consteval & constinit
C++ can run a surprising amount of your code while it compiles. A value that the compiler has already calculated costs nothing at run time, and a mistake that the compiler finds never reaches a customer. Libraries use this heavily: std::format checks your format strings at compile time, and lookup tables in games, compression and networking code are often computed by the compiler.
You met constexpr variables in Variables & Data Types and a constexpr function in Functions, Overloading & Defaults. This lesson goes further: compile-time tables, std::vector inside constexpr code, consteval functions that must run at compile time, if consteval, and constinit for safe global variables.
constexpr functions: compile time when needed
A constexpr function can run at compile time, but it is not forced to. It runs at compile time when its result is needed in a constant context: initialising a constexpr variable, a static_assert, an array size or a template argument. Called with ordinary run-time values, it is just a normal function. Since C++20 almost any ordinary code is allowed inside: loops, local variables, std::array, and even try blocks and virtual calls.
Here a function builds a table of prime numbers. The constexpr variable forces the compiler to run it, and the finished table is stored in the program like any constant.
#include <array>
#include <cstddef>
#include <print>
template <std::size_t N>
constexpr std::array<int, N> first_primes() {
std::array<int, N> result{};
std::size_t found = 0;
for (int n = 2; found < N; ++n) {
bool prime = true;
for (int d = 2; d * d <= n; ++d) {
if (n % d == 0) {
prime = false;
break;
}
}
if (prime) {
result[found++] = n;
}
}
return result;
}
constexpr auto primes = first_primes<10>(); // computed by the compiler
static_assert(primes[9] == 29, "the tenth prime is 29");
int main() {
for (std::size_t i = 0; i < primes.size(); ++i) {
std::print("{}{}", i == 0 ? "" : ", ", primes[i]);
}
std::println("");
}2, 3, 5, 7, 11, 13, 17, 19, 23, 29
If you broke the prime test, the static_assert would stop the build with the message you wrote, instead of shipping a wrong table. Compilers limit how much work they will do at compile time (loop steps and recursion depth), so very large tables may need a compiler option or a different approach.
std::vector and std::string inside constexpr code
Since C++20, std::vector and std::string can be used in constexpr functions (GCC 12, Clang 15 and Visual Studio 2019 16.10 were the first to support this). There is one rule: memory allocated during compile-time evaluation must also be freed before that evaluation ends. You can use a vector as a temporary helper, but you cannot keep it in a constexpr variable.
#include <algorithm>
#include <initializer_list>
#include <print>
#include <vector>
constexpr int count_distinct(std::initializer_list<int> marks) {
std::vector<int> v(marks);
std::ranges::sort(v);
auto [first, last] = std::ranges::unique(v);
v.erase(first, last);
return static_cast<int>(v.size()); // the vector is freed here: allowed
}
static_assert(count_distinct({78, 92, 78, 65, 92}) == 3);
int main() {
std::println("Distinct marks: {}", count_distinct({40, 40, 40, 99}));
}Distinct marks: 2
The same function works at compile time (in the static_assert) and at run time (in main). Returning an int or a std::array from such a function is the usual way to keep the result.
consteval: compile time or nothing
A consteval function is an immediate function: every call must be evaluated at compile time. If the compiler cannot do that, the program does not compile. This makes consteval perfect for validating constants written in code. Indian PIN codes have six digits and never start with 0:
#include <print>
#include <string_view>
consteval int pin_code(std::string_view text) {
if (text.size() != 6) {
throw "a PIN code has exactly 6 digits";
}
if (text[0] == '0') {
throw "a PIN code cannot start with 0";
}
int value = 0;
for (char c : text) {
if (c < '0' || c > '9') {
throw "a PIN code has only digits";
}
value = value * 10 + (c - '0');
}
return value;
}
constexpr int rohtak = pin_code("124001");
constexpr int kochi = pin_code("682001");
// constexpr int typo = pin_code("12400"); // build fails: the throw runs at compile time
int main() {
std::println("Rohtak {}, Kochi {}", rohtak, kochi);
}Rohtak 124001, Kochi 682001
Reaching a throw during compile-time evaluation is not allowed, so a bad PIN code turns into a compiler error that points at the throw line, and the message string appears in the error. A consteval function also cannot be called with values that are only known at run time, such as text the user types; for that you need a normal function that returns an error.
if consteval: one function, two paths
Sometimes the fastest run-time code (an assembly routine, a hardware instruction, a C library call) cannot run at compile time. C++23's if consteval lets one function choose: the first block runs during compile-time evaluation, the else block at run time.
#include <cmath>
#include <print>
constexpr double square_root(double x) {
if consteval {
double guess = x / 2; // Newton's method: works at compile time
for (int i = 0; i < 20; ++i) {
guess = (guess + x / guess) / 2;
}
return guess;
} else {
return std::sqrt(x); // fast library call at run time
}
}
constexpr double root_two = square_root(2.0);
int main() {
double area = 1299.0;
std::println("{:.6f} {:.6f}", root_two, square_root(area));
}1.414214 36.041643
if consteval needs no condition in brackets, and the braces are required. Before C++23, code used if (std::is_constant_evaluated()) from <type_traits> (C++20) for the same purpose; you will still see it in libraries.
constinit: safe global variables
Global and static variables are initialised before main starts. If one global's initial value depends on a global in another .cpp file, the order between files is not defined, and the program may read a variable before it is set up. This is the famous static initialisation order fiasco. constinit (C++20) guarantees that a variable is initialised at compile time, while still allowing it to change later:
#include <print>
constexpr int gst_percent = 18; // compile time AND read-only
constinit int max_users = 500; // compile time, but can change
// constinit int bad = std::rand(); // build fails: not a constant initialiser
int main() {
max_users += 250; // allowed: constinit is not const
std::println("GST {}%, users allowed {}", gst_percent, max_users);
}GST 18%, users allowed 750
constinit can only be used on variables with static or thread storage duration (globals, static locals and members, thread_local), not on ordinary local variables.
| Keyword | Value known at compile time? | Can it change later? | Applies to |
|---|---|---|---|
const | Not required | No | Any variable |
constexpr | Yes | No | Variables and functions (may run at compile time) |
consteval | Yes, always | (functions only) | Functions that must run at compile time |
constinit | Yes | Yes | Static and thread-local variables |
Common mistakes
- Expecting
constexprfunctions to always run at compile time. They do only in a constant context. Use aconstexprvariable orconstevalto force it. - Keeping a
std::vectorin aconstexprvariable. Compile-time allocations must be freed; return anstd::arrayor a number instead. - Using
constiniton a local variable. It is only for static and thread-local variables. - Confusing
constinitwithconst. Aconstinitvariable can be modified; addconstif it must not change, or simply useconstexpr. - Huge compile-time loops. They slow every build and can hit compiler limits.
Key points
constexprfunctions run at compile time when used in a constant context, and normally otherwise.- C++20 allows
std::vectorandstd::stringinconstexprcode if all memory is freed before evaluation ends. constevalfunctions must run at compile time; athrowreached there becomes a compiler error, which is useful for validation.- C++23
if constevalchooses between a compile-time path and a run-time path. constinitguarantees compile-time initialisation of globals and avoids the static initialisation order problem.
Exercise
Write a consteval function int rupees_to_paise(int rupees) that throws if the amount is negative or greater than 1,00,000 (one lakh), and returns the amount in paise. Use it to define constexpr constants for a ₹1,299 kurta and a ₹450 dupatta, add a static_assert that the kurta is more expensive, and print both values.
Show solution
#include <print>
consteval int rupees_to_paise(int rupees) {
if (rupees < 0 || rupees > 100'000) {
throw "price out of range";
}
return rupees * 100;
}
constexpr int kurta = rupees_to_paise(1299);
constexpr int dupatta = rupees_to_paise(450);
static_assert(kurta > dupatta, "the kurta should cost more");
int main() {
std::println("Kurta: {} paise, dupatta: {} paise", kurta, dupatta);
}Kurta: 129900 paise, dupatta: 45000 paise
Writing rupees_to_paise(-5) or rupees_to_paise(250'000) anywhere in the code stops the build, so wrong prices can never be compiled into the program.
Quick Quiz
1. When is a constexpr function guaranteed to run at compile time?
Outside constant contexts it can run as a normal run-time function.
2. What happens if a consteval function reaches a throw while compiling?
Throwing is not allowed during constant evaluation, so the call is not a constant expression and the build fails.
3. Which keyword makes a global initialised at compile time but still changeable?
constinit only fixes how the variable starts; it does not make it const.
4. Can a constexpr function use std::vector in C++20?
Transient allocations are allowed; keeping them in a constant is not.
Learn C++ with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
