Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compile-time C++

constexpr deeply

constexpr marks a function, variable, or constructor as eligible for constant evaluation. The standard defines a constant expression as an expression that can be evaluated during translation when all operands are themselves constant. When the compiler encounters a call to a constexpr function in such a context, it substitutes the computed value directly into the program.

Since C++14 a constexpr function can contain loops, local variables, and if statements, so its body resembles ordinary code. The constant‑evaluation engine still enforces a sandbox: no I/O, no asm, and no use of the address of a non‑constant object. If a call cannot be evaluated, the compiler either falls back to a runtime call where legal or issues a hard error in a context that requires a constant expression.

Since C++14 a constexpr function can contain loops, local variables, and if statements, so the body looks like ordinary code. The compiler still enforces a constant-evaluation sandbox: no I/O, no asm, and no use of the address of a non-constant object. When a call cannot be evaluated, the compiler either falls back to a runtime call where that is legal, or reports a hard error in a context that demands a constant expression.

The most common pattern is a recursive algorithm that terminates at compile time. The classic example is factorial:

#include <iostream>

constexpr long long factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

static_assert(factorial(5) == 120, "factorial compile‑time test");

int main() {
    std::cout << "factorial ok\n";
    return 0;
}

The static‑assert in the file forces the compiler to evaluate factorial(5) at compile time. The result of 120 becomes part of the program’s constant pool. The main function prints a short marker so the book’s test harness can verify that the binary linked and executed. This example also demonstrates that constexpr functions can be called from other constexpr contexts, such as template non‑type parameters, std::array sizes, or static_assert conditions.

consteval (immediate functions)

consteval is a stronger guarantee introduced in C++20. An immediate function must be evaluated at translation time. Any attempt to call it where a constant expression is not required is ill‑formed. The compiler therefore rejects the program outright, which produces a diagnostic that points to the offending call site. Immediate functions are ideal for compile‑time utilities that must never appear in the generated binary, such as compile‑time string hashing, type‑level identifiers, or compile‑time parsing of literals.

The following example computes a simple additive hash of a string literal. Because the function is declared consteval, the call hash("abc") is forced into the constant‑evaluation engine. The resulting value is verified with a static_assert. The main function prints a marker that confirms the program compiled successfully.

#include <iostream>
#include <cstddef>

// Very simple compile‑time hash: sum of character codes.
consteval std::size_t hash(const char* str) {
    std::size_t h = 0;
    for (std::size_t i = 0; str[i] != '\0'; ++i) {
        h += static_cast<std::size_t>(str[i]);
    }
    return h;
}

static_assert(hash("abc") == ('a' + 'b' + 'c'), "hash compile‑time test");

int main() {
    std::cout << "hash ok\n";
    return 0;
}

If a programmer later tries to invoke hash with a run‑time string, the compilation fails with a clear message: call to a consteval function is not a constant expression.

Prefer consteval when the function exists only to compute compile-time values, such as a hash or a table generator, because it removes the runtime path entirely and lets the optimizer assume the result is a constant. Prefer constexpr when the same logic also serves runtime inputs, as a parser or a math helper does.

constinit

Static or thread‑local objects with static storage duration are normally zero‑initialized first and then later given their dynamic initializer. This two‑step process can lead to the infamous static‑initialization‑order fiasco when one translation unit accesses a global defined in another before its dynamic initializer runs. The constinit specifier forces the initializer to be a constant expression, guaranteeing that the object is fully initialized before any dynamic initialization begins.

The example below defines a global counter whose value is computed in a constinit variable. The lambda runs at compile time, sums the numbers 0 through 4, and the result 10 becomes the object’s constant initial value. A static_assert validates the result, and the program prints a marker.

#include <iostream>

// Compute a compile‑time sum using a constexpr lambda.
constinit const int global_counter = []constexpr noexcept{
    int x = 0;
    for (int i = 0; i < 5; ++i) x += i;
    return x;
}();

static_assert(global_counter == 10, "constinit compile‑time init");

int main() {
    std::cout << "constinit ok\n";
    return 0;
}

Because global_counter is constinit, the compiler must emit an error if its initializer cannot be evaluated at compile time. This eliminates a whole class of runtime bugs without any additional runtime checks.

constinit differs from a constexpr variable. A constexpr variable is itself constant and can be used in constant expressions. A constinit variable can be non-constant, so it can be mutated later, but its initializer must be constant. Use constinit for mutable globals that must avoid the initialization-order fiasco.

std::is_constant_evaluated

Inside a constexpr function the standard library provides std::is_constant_evaluated(). It returns true when the current evaluation is performed by the constant‑evaluation engine, and false when the function runs at run time. This enables a single implementation to take two distinct paths: a highly optimized compile‑time algorithm and a more flexible run‑time fallback.

The branch example below illustrates this technique. When called inside a static_assert, the compile‑time branch returns 0. When called from main, the run‑time branch prints the word runtime and returns the argument unchanged. The static‑assert confirms the compile‑time result, and the program output shows the run‑time branch execution.

#include <iostream>
#include <type_traits>

constexpr int branch(int x) {
    if (std::is_constant_evaluated()) {
        return 0; // compile‑time path
    } else {
        std::cout << "runtime\n";
        return x;
    }
}

static_assert(branch(42) == 0, "branch compile‑time result");

int main() {
    constexpr int result = branch(7);
    std::cout << "branch result: " << result << "\n";
    return 0;
}

Using std::is_constant_evaluated is a common idiom for providing cheap compile‑time shortcuts without sacrificing generic run‑time behavior. It also avoids the need for separate overloads guarded by if constexpr in user code.

The same test appears inside the standard library. std::vector and std::string check is_constant_evaluated internally to choose an allocation-free path during constant evaluation. Recognizing this pattern helps you understand why a container can be constexpr when a raw allocation cannot be.

Transient constexpr allocation

C++20 lifted the restriction that dynamic allocation cannot appear in a constant expression. The rule states that any allocation performed during constant evaluation is transient: the allocated storage exists only for the duration of the evaluation and is reclaimed automatically when the evaluation finishes. This makes it possible to build containers such as std::vector or std::string inside a constexpr function, as long as the container does not escape the evaluation.

The example builds a std::array via a constexpr helper that fills the elements with squared indices. Although std::array does not allocate dynamically, the pattern demonstrates how a compile‑time algorithm can populate a fixed‑size aggregate. The static_assert checks the final element, and the program prints a marker.

#include <iostream>
#include <array>

constexpr std::array<int,5> make_array() {
    std::array<int,5> a{};
    for (std::size_t i = 0; i < a.size(); ++i) a[i] = static_cast<int>(i * i);
    return a;
}

constexpr auto arr = make_array();
static_assert(arr[4] == 16, "constexpr array element");

int main() {
    std::cout << "array ok\n";
    return 0;
}

On compilers that fully support transient allocation, the same pattern can be written with std::vector:

constexpr std::vector<int> make_vec() {
    std::vector<int> v;
    for (int i = 0; i < 5; ++i) v.push_back(i * i);
    return v; // OK in C++20 and later
}

If the toolchain does not yet implement this feature, the fallback to a fixed‑size container still provides a compile‑time constant data structure.

A practical consequence is that compile-time data tables can be built with ordinary loops and containers, then baked into the binary as constants. This removes both the runtime construction cost and the need to hand-write static initializer lists.

Compile‑time as pure evaluation (Lisp framing)

A constexpr function behaves like a pure term‑rewriter: given inputs it produces an output without observable side effects. The compiler treats it as a mathematical function, can memoize results for identical constant arguments, and folds the value into the program. This mirrors the term‑rewriting semantics of templates introduced in Chapter 16, where the compile‑time engine performs substitution, checks constraints, and yields a result.

When invoked repeatedly with the same constant arguments, the compiler can emit the value once and reuse it, reducing code size and eliminating redundant work. This is analogous to a Lisp interpreter evaluating a pure function at compile time and storing the result for later calls.

The convention flip: static‑assert test suites

Historically, examples printed values and asked the reader to run the program and verify the output manually. With constexpr, the result is known at compile time, so the testing strategy flips to compile‑time verification: a static_assert encodes the expectation directly in the source file. The compiler checks it during translation. A failure stops the build with a clear diagnostic reported by the CI pipeline. The chapter still includes a short std::cout marker so the CTest harness can confirm that the binary linked and executed.

C++26 constexpr frontiers

C++26 expands the constexpr toolbox with several ambitious features that are not yet supported by the Clang 22.1.8 toolchain used for verification.

Not yet deployable. constexpr placement‑new permits constructing an object in a pre‑allocated buffer during constant evaluation. Example syntax:

constexpr char buffer[sizeof(MyType)];
constexpr MyType* p = new (buffer) MyType{ /* args */ };

Not yet deployable. constexpr exception handling makes it possible to throw and catch inside a constant expression. Example syntax:

constexpr int safe_div(int a, int b) {
    if (b == 0) throw std::runtime_error("divide by zero");
    return a / b;
}
static_assert(safe_div(4,2) == 2);

Not yet deployable. User‑generated static_assert messages can incorporate constexpr data, which allows a compile‑time error to report a computed value. Example syntax:

template<int N>
struct prime_checker {
    static constexpr bool value = /* primality test */;
    static_assert(value, "Value " #N " is not prime");
};

These proposals (P2002R2 for placement‑new, P2003R1 for constexpr exceptions, and P2004R0 for message templating) are documented in the C++26 draft but remain unavailable in the current verification environment. The book marks them with a callout so readers understand the future direction without being blocked by the existing toolchain.

Try this

Write a constexpr function is_prime that returns true if its argument is a prime number and false otherwise. Add static_assert checks for a few known values and print a marker from main.