EthicsComputerCourses
Join training +91 90532 10052

constexpr, consteval & constinit

C++ · Lesson 32 of 46 Advanced 7 min read Updated

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.

C++
#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("");
}
Output
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.

C++
#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}));
}
Output
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:

C++
#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);
}
Output
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.

C++
#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));
}
Output
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:

C++
#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);
}
Output
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.

KeywordValue known at compile time?Can it change later?Applies to
constNot requiredNoAny variable
constexprYesNoVariables and functions (may run at compile time)
constevalYes, always(functions only)Functions that must run at compile time
constinitYesYesStatic and thread-local variables

Common mistakes

  • Expecting constexpr functions to always run at compile time. They do only in a constant context. Use a constexpr variable or consteval to force it.
  • Keeping a std::vector in a constexpr variable. Compile-time allocations must be freed; return an std::array or a number instead.
  • Using constinit on a local variable. It is only for static and thread-local variables.
  • Confusing constinit with const. A constinit variable can be modified; add const if it must not change, or simply use constexpr.
  • Huge compile-time loops. They slow every build and can hit compiler limits.

Key points

  • constexpr functions run at compile time when used in a constant context, and normally otherwise.
  • C++20 allows std::vector and std::string in constexpr code if all memory is freed before evaluation ends.
  • consteval functions must run at compile time; a throw reached there becomes a compiler error, which is useful for validation.
  • C++23 if consteval chooses between a compile-time path and a run-time path.
  • constinit guarantees 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
C++
#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);
}
Output
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?

2. What happens if a consteval function reaches a throw while compiling?

3. Which keyword makes a global initialised at compile time but still changeable?

4. Can a constexpr function use std::vector in C++20?

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