Compile-Time Computation Evolution
Modern C++ continuously expands compile-time capabilities. C++20 and C++23 bring significant improvements to constexpr and introduce consteval and constinit.
constexpr Evolution
C++11: Basic constexpr Functions
constexpr int square(int x) {
return x * x;
}
constexpr int result = square(5); // Computed at compile-time
int runtime_val = square(n); // Can still be used at runtime
C++14: Relaxed constexpr
Allows multiple statements, loops, and local variables:
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int fact5 = factorial(5); // 120, computed at compile-time
C++17: constexpr Lambda
auto squared = [](int x) constexpr { return x * x; };
constexpr int val = squared(10); // 100
C++20: Major Expansions
Virtual functions, try-catch, dynamic allocation, and more:
#include <vector>
#include <algorithm>
constexpr int sum_vector() {
std::vector<int> vec{1, 2, 3, 4, 5}; // Dynamic allocation!
int sum = 0;
for (int x : vec) {
sum += x;
}
return sum;
}
constexpr int result = sum_vector(); // 15, at compile-time
consteval: Immediate Functions (C++20)
Functions that must execute at compile-time:
consteval int compile_time_only(int x) {
return x * x;
}
constexpr int value1 = compile_time_only(5); // OK: compile-time
int n = 5;
int value2 = compile_time_only(n); // ERROR: not compile-time
Use Cases for consteval
Generate compile-time constants:
consteval unsigned hash_string(const char* str) {
unsigned hash = 0;
while (*str) {
hash = hash * 31 + *str++;
}
return hash;
}
switch (event_type) {
case hash_string("click"): // Computed at compile-time
handle_click();
break;
case hash_string("keypress"):
handle_key();
break;
}
consteval vs constexpr
constexpr int maybe_compile_time(int x) {
return x * 2;
}
consteval int always_compile_time(int x) {
return x * 2;
}
int runtime_value = 10;
int a = maybe_compile_time(5); // OK: compile-time
int b = maybe_compile_time(runtime_value); // OK: runtime
int c = always_compile_time(5); // OK: compile-time
int d = always_compile_time(runtime_value); // ERROR!
constinit: Compile-Time Initialization (C++20)
Ensures variables are initialized at compile-time, but allows runtime modification:
constinit int global_counter = 42; // Must be compile-time initialized
void increment() {
++global_counter; // OK: can modify at runtime
}
// constexpr would make it immutable
constexpr int immutable = 42;
// immutable = 43; // ERROR: cannot modify
// constinit ensures initialization, allows modification
constinit int mutable_init = compute_value(); // compute_value() must be constexpr
Static Storage Duration
constinit is primarily for static/thread-local variables:
constinit static int initialized_once = expensive_computation();
// Guarantees no dynamic initialization order issues
Prevents Static Initialization Order Fiasco
// header.h
constinit extern int global_config; // Declaration
// source.cpp
constinit int global_config = compute_config(); // Must be compile-time
// other.cpp
// Safe to use global_config - guaranteed initialized
int value = global_config + 10;
if consteval (C++23)
Different code paths for compile-time vs runtime:
constexpr int compute(int x) {
if consteval {
// This branch runs only at compile-time
return expensive_compile_time_computation(x);
} else {
// This branch runs only at runtime
return fast_runtime_computation(x);
}
}
Practical Example
consteval int compile_time_sqrt(int x) {
// Simple algorithm acceptable for compile-time
int result = 0;
while (result * result < x) ++result;
return result;
}
int runtime_sqrt(int x) {
// Optimized runtime algorithm
return std::sqrt(x);
}
constexpr int safe_sqrt(int x) {
if consteval {
return compile_time_sqrt(x);
} else {
return runtime_sqrt(x);
}
}
constexpr std::string and std::vector (C++20)
constexpr std::string build_greeting() {
std::string greeting = "Hello, ";
greeting += "World!";
return greeting;
}
constexpr auto msg = build_greeting(); // Compile-time!
constexpr auto compute_primes(int max) {
std::vector<int> primes;
for (int n = 2; n <= max; ++n) {
bool is_prime = true;
for (int p : primes) {
if (n % p == 0) {
is_prime = false;
break;
}
}
if (is_prime) primes.push_back(n);
}
return primes;
}
constexpr auto primes = compute_primes(20);
// std::array or similar at compile-time
constexpr Virtual Functions (C++20)
struct Shape {
virtual constexpr double area() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
double radius;
constexpr Circle(double r) : radius(r) {}
constexpr double area() const override {
return 3.14159 * radius * radius;
}
};
constexpr double compute_area() {
Circle c{5.0};
Shape& s = c;
return s.area(); // Virtual call at compile-time!
}
constexpr double area = compute_area(); // 78.5398...
try-catch in constexpr (C++20)
constexpr int safe_divide(int a, int b) {
try {
if (b == 0) throw std::runtime_error("Division by zero");
return a / b;
} catch (...) {
return 0; // Error value
}
}
constexpr int result = safe_divide(10, 2); // 5
Note: Throwing exceptions causes compile-time evaluation to fail, but the structure is allowed.
std::is_constant_evaluated()
Detect compile-time evaluation:
#include <type_traits>
constexpr int compute(int x) {
if (std::is_constant_evaluated()) {
// Compile-time: use simple algorithm
return x * x;
} else {
// Runtime: can use optimized version
return fast_multiply(x, x);
}
}
Practical Examples
Compile-Time Configuration
consteval auto get_build_config() {
struct Config {
const char* version;
int optimization_level;
bool debug;
};
return Config{
.version = "1.0.0",
.optimization_level = 2,
.debug = false
};
}
constexpr auto config = get_build_config();
Compile-Time Validation
consteval void validate_range(int min, int max) {
if (min >= max) {
throw "Invalid range"; // Compile-time error
}
}
template<int Min, int Max>
struct Range {
Range() {
validate_range(Min, Max); // Compile-time check
}
};
Range<1, 10> valid; // OK
Range<10, 1> invalid; // Compile error!
Conceptual Check
What is the key difference between constexpr and consteval functions?
Interactive Lab
Complete the Code
// Create a function that MUST execute at compile-time int cube(int x) { return x * x * x; } constexpr int result = cube(3); // Must work at compile-time
Runtime Environment
Interactive Lab
1#include <iostream>
2
3constexpr int factorial(int n) {
4 int result = 1;
5 for (int i = 2; i <= n; ++i) {
6 result *= i;
7 }
8 return result;
9}
10
11consteval int square(int x) {
12 return x * x;
13}
14
15int main() {
16 constexpr int fact5 = factorial(5);
17 constexpr int sq10 = square(10);
18
19 std::cout << "5! = " << fact5 << '\n';
20 std::cout << "10² = " << sq10 << '\n';
21
22 // This works:
23 int n = 5;
24 std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
25
26 // This would fail: square(n) - consteval requires compile-time
27
28 return 0;
29}
System Console
Waiting for signal...