Phase 4 · Advanced C++ & Systems ProgrammingModule 22~60 min read

Compile-Time Programming & Metaprogramming

Move appropriate computation and validation to compile time with constexpr, consteval, type traits, folds, and constrained metaprogramming.

What you'll learn

Compile-time programming can validate configuration, derive types, and precompute small results before a program runs. Modern C++ favors readable constexpr, concepts, traits, andif constexpr over deeply recursive template tricks.

By the end, you'll be able to:

  • Use constexpr functions and objects in constant and runtime contexts
  • Distinguish consteval and constinit from constexpr
  • Branch on types with traits and if constexpr
  • Balance compile-time guarantees against diagnostics and build cost

Constant expressions

A constant expression can be evaluated during translation under language rules that exclude runtime-only operations. A constexpr function is eligible for compile-time evaluation when called with suitable inputs and can still run at runtime for dynamic inputs.

constexpr.cpp
#include <array>
#include <cstddef>
#include <iostream>

constexpr unsigned factorial(unsigned value) {
    unsigned result{1};
    for (unsigned current{2}; current <= value; ++current) result *= current;
    return result;
}

static_assert(factorial(5) == 120);
std::array<int, factorial(3)> values{};

int main() {
    unsigned input{4};
    std::cout << factorial(input) << ' ' << values.size() << '\n';
}

Key idea

constexpr means “can participate in constant evaluation,” not “always runs at compile time.” The use site and arguments determine whether evaluation must be constant.

consteval and constinit

A consteval immediate function must produce a constant result at every potentially evaluated call. constinit applies to static or thread-storage variables and requires static initialization, preventing dynamic initialization order surprises; it does not make the variable immutable.

SpecifierPrimary guarantee
constexprEligible or required constant value, depending on declaration/use
constevalEvery call is an immediate constant evaluation
constinitStatic initialization for a static/thread variable
constMutation through this object/name is forbidden
compile_time_id.cpp
#include <cstdint>
#include <string_view>

consteval std::uint32_t id(std::string_view text) {
    std::uint32_t hash{2166136261u};
    for (char character : text) {
        hash ^= static_cast<unsigned char>(character);
        hash *= 16777619u;
    }
    return hash;
}

constexpr auto save_id{id("save")};
constinit unsigned startup_count{0};

Compile-time containers and algorithms

Literal-friendly standard types such as std::array, std::string_view, and many algorithms can work in constant evaluation. This is useful for lookup tables, verified metadata, and small deterministic transformations—not for moving every workload into compilation.

sorted_table.cpp
#include <algorithm>
#include <array>

constexpr auto sorted_codes() {
    std::array codes{42, 7, 19, 3};
    std::ranges::sort(codes);
    return codes;
}

constexpr auto codes{sorted_codes()};
static_assert(codes[0] == 3);
static_assert(codes[3] == 42);

Tip

Use static_assert to pin important compile-time facts close to their definition. Its message should explain the domain rule, not merely repeat the Boolean expression.

Type traits and if constexpr

Type traits compute facts or transformations about types. Most Boolean traits expose a_v value and transformations expose a _t alias. Anif constexpr discards the non-selected branch during instantiation, allowing one generic function to use type-specific implementations safely.

describe.cpp
#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
void describe(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "integer " << value << '\n';
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "floating " << value << '\n';
    } else {
        std::cout << "other\n";
    }
}

int main() { describe(7); describe(2.5); describe(std::string{"hi"}); }

Note

Prefer a concept or constrained overload when branches represent distinct public contracts. Use if constexpr for localized implementation variation inside one coherent operation.

Folds, detection, and constraints

Fold expressions reduce parameter packs without recursive template scaffolding. Requires- expressions provide modern detection: they test whether expressions, types, or constraints are valid and feed those facts into a named concept or constrained overload.

hashable.cpp
#include <concepts>
#include <cstddef>
#include <functional>

template <typename T>
concept Hashable = requires(const T& value) {
    { std::hash<T>{}(value) } -> std::convertible_to<std::size_t>;
};

template <Hashable... Values>
std::size_t combine_hashes(const Values&... values) {
    std::size_t seed{};
    ((seed ^= std::hash<Values>{}(values) + 0x9e3779b9u
              + (seed << 6) + (seed >> 2)), ...);
    return seed;
}

Watch out

A syntactic requirement proves only that an expression is well-formed. Semantic rules—such as equal values having equal hashes—still require documentation, tests, and correct implementations.

Diagnostics and tradeoffs

Compile-time work can reduce runtime cost and reject invalid configurations, but it can also increase build time, binary size, and diagnostic complexity. Measure clean and incremental builds, keep metaprogramming behind small interfaces, and prefer ordinary readable code when a runtime check is cheap and sufficient.

  • Move stable tables and invariants to compile time when it improves correctness
  • Avoid generating a specialization for every incidental combination
  • Constrain early so diagnostics point to the public call
  • Test compile-time and runtime paths of dual-use constexpr functions
  • Profile build time and binary size as real engineering metrics

Recap & quick check

Key takeaways

  • constexpr supports both constant and runtime evaluation; the context decides which occurs.
  • consteval requires immediate evaluation, while constinit guarantees static initialization.
  • Compile-time standard containers and algorithms can build verified small data structures.
  • Traits and if constexpr adapt implementations; concepts express public semantic requirements.
  • Compile-time computation trades runtime work for build time, diagnostics, and possible code growth.

Quick check

1. Does a constexpr function always run at compile time?

2. What does consteval require?

3. What happens to the unselected branch of if constexpr?

4. What is a major compile-time-programming tradeoff?

Next: Module 23 — Concurrency & the C++ Memory Model, where correctness must hold across interleaved execution and shared memory.