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.
#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
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.
| Specifier | Primary guarantee |
|---|---|
constexpr | Eligible or required constant value, depending on declaration/use |
consteval | Every call is an immediate constant evaluation |
constinit | Static initialization for a static/thread variable |
const | Mutation through this object/name is forbidden |
#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.
#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
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.
#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
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.
#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
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.