C++ constexpr & Compile-Time Computing: consteval, if constexpr 2026
Table of Contents
1. Why Compile-Time Computation?
Compile-time computation lets the compiler do work that would otherwise happen at runtime. The result is code that’s faster (zero runtime cost), safer (errors caught at compile time), and more expressive.
// Runtime: computed every time the program runs
int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
int val = factorial(10); // computed at runtime
// Compile-time: computed once by the compiler
constexpr int factorialCE(int n) { return n <= 1 ? 1 : n * factorialCE(n - 1); }
constexpr int val = factorialCE(10); // computed at compile time!
// The binary literally contains 3628800 — no multiplication at runtime
2. constexpr Variables
constexpr variables must be initialized with compile-time constants:
#include <cmath>
#include <array>
// constexpr variables: guaranteed compile-time
constexpr int MAX_SIZE = 1024;
constexpr double PI = 3.14159265358979;
constexpr int TABLE_SIZE = MAX_SIZE * 2; // expressions of constexpr are constexpr
// Can be used where compile-time constants are required
std::array<int, MAX_SIZE> buffer; // template argument
static_assert(MAX_SIZE > 0); // static assertion
int arr[TABLE_SIZE]; // C-style array size
// constexpr vs const
const int a = 42; // const: might be compile-time
constexpr int b = 42; // constexpr: guaranteed compile-time
const int c = getValue(); // const: runtime value (OK)
// constexpr int d = getValue(); // Error: getValue() not constexpr
// constexpr implies const for variables
constexpr int x = 10;
// x = 20; // Error: x is const
3. constexpr Functions
constexpr functions can be evaluated at compile time when called with constant expressions, but also work at runtime:
#include <iostream>
constexpr int power(int base, int exp) {
int result = 1;
for (int i = 0; i < exp; ++i)
result *= base;
return result;
}
// C++14+: constexpr functions can have:
// - local variables, loops, if/else
// - multiple statements and return points
constexpr int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
// C++20: constexpr functions can use:
// - try/catch (but no throw at compile time)
// - dynamic allocation (new/delete) if freed before function returns
// - virtual functions
// - std::vector and std::string (in many compilers)
constexpr auto makeSquares() {
std::array<int, 10> arr{};
for (int i = 0; i < 10; ++i)
arr[i] = i * i;
return arr;
}
int main() {
// Compile-time evaluation
constexpr int p = power(2, 10); // 1024 at compile time
constexpr int f = fibonacci(20); // 6765 at compile time
constexpr auto squares = makeSquares();
static_assert(p == 1024);
static_assert(f == 6765);
static_assert(squares[5] == 25);
// Runtime evaluation (same function works at runtime too)
int n;
std::cin >> n;
int result = power(2, n); // computed at runtime
std::cout << "2^" << n << " = " << result << std::endl;
}
4. constexpr Classes
#include <cmath>
class Point {
double x_, y_;
public:
constexpr Point(double x, double y) : x_(x), y_(y) {}
constexpr double x() const { return x_; }
constexpr double y() const { return y_; }
constexpr double distanceSquared(const Point& other) const {
double dx = x_ - other.x_;
double dy = y_ - other.y_;
return dx * dx + dy * dy;
}
constexpr Point operator+(const Point& other) const {
return Point(x_ + other.x_, y_ + other.y_);
}
constexpr bool operator==(const Point& other) const {
return x_ == other.x_ && y_ == other.y_;
}
};
constexpr Point origin(0, 0);
constexpr Point p1(3, 4);
constexpr Point p2 = origin + p1;
static_assert(p2.x() == 3.0);
static_assert(p1.distanceSquared(origin) == 25.0);
// Compile-time lookup table
class ColorMap {
struct Entry { int id; const char* name; };
Entry entries_[4];
int size_;
public:
constexpr ColorMap() : entries_{{0,"red"},{1,"green"},{2,"blue"},{3,"alpha"}}, size_(4) {}
constexpr const char* lookup(int id) const {
for (int i = 0; i < size_; ++i)
if (entries_[i].id == id) return entries_[i].name;
return "unknown";
}
};
constexpr ColorMap colors;
static_assert(colors.lookup(1)[0] == 'g'); // "green" at compile time
5. if constexpr (C++17)
if constexpr evaluates conditions at compile time and discards the false branch entirely — essential for template metaprogramming:
#include <type_traits>
#include <string>
#include <iostream>
// Generic serializer using if constexpr
template<typename T>
std::string serialize(const T& value) {
if constexpr (std::is_integral_v<T>) {
return "int:" + std::to_string(value);
} else if constexpr (std::is_floating_point_v<T>) {
return "float:" + std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return "string:" + value;
} else {
static_assert(sizeof(T) == 0, "Unsupported type");
}
}
// Variadic print with if constexpr
template<typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
print(rest...); // Only instantiated if there are more args
}
}
// Container vs single value
template<typename T>
void display(const T& value) {
if constexpr (requires { value.begin(); value.end(); }) {
// T is iterable
for (const auto& elem : value)
std::cout << elem << " ";
} else {
// T is a single value
std::cout << value;
}
std::cout << "\n";
}
int main() {
std::cout << serialize(42) << "\n"; // int:42
std::cout << serialize(3.14) << "\n"; // float:3.140000
std::cout << serialize(std::string("hi")) << "\n"; // string:hi
print(1, "hello", 3.14, 'x'); // 1, hello, 3.14, x
std::cout << "\n";
display(42); // 42
display(std::vector<int>{1, 2, 3}); // 1 2 3
}
6. consteval (C++20)
consteval functions must be evaluated at compile time — they can never run at runtime:
// consteval: guaranteed compile-time only
consteval int compileTimeOnly(int n) {
return n * n;
}
constexpr int a = compileTimeOnly(5); // OK: compile time
// int b = compileTimeOnly(getUserInput()); // Error: not compile time!
// Useful for compile-time validation
consteval int checkedArraySize(int n) {
if (n <= 0 || n > 10000)
throw "Invalid array size"; // Compile error if triggered
return n;
}
std::array<int, checkedArraySize(100)> arr; // OK
// std::array<int, checkedArraySize(-1)> bad; // Compile error!
// consteval for source location (compile-time logging)
#include <source_location>
consteval auto getCallerInfo(
std::source_location loc = std::source_location::current()) {
return loc;
}
void example() {
constexpr auto info = getCallerInfo();
// info.line(), info.file_name(), info.function_name()
// all known at compile time
}
7. constinit (C++20)
constinit ensures a variable is initialized at compile time but doesn’t make it const — solving the static initialization order fiasco:
// The problem: static initialization order is undefined across translation units
// File A:
// extern int y;
// int x = y + 1; // y might not be initialized yet!
// constinit solves this:
constinit int globalCounter = 0; // Guaranteed initialized at compile time
// But can be modified at runtime:
void increment() { ++globalCounter; } // OK: not const
// constinit requires constant initialization
constinit static int value = computeAtCompileTime(42); // Must be constexpr-able
// constinit static int bad = computeAtRuntime(); // Error!
// Comparison:
// const: possibly runtime init, immutable
// constexpr: compile-time init, immutable
// constinit: compile-time init, mutable
8. Practical Applications
Compile-Time Lookup Tables
constexpr auto buildSinTable() {
std::array<double, 360> table{};
for (int i = 0; i < 360; ++i)
table[i] = /* approximate sin(i * PI / 180) */
(i * 3.14159265 / 180.0); // simplified
return table;
}
constexpr auto SIN_TABLE = buildSinTable(); // computed at compile time
Type-Safe Unit Conversions
template<typename Tag, typename T = double>
class Unit {
T value_;
public:
constexpr explicit Unit(T v) : value_(v) {}
constexpr T get() const { return value_; }
};
struct MetersTag {};
struct FeetTag {};
using Meters = Unit<MetersTag>;
using Feet = Unit<FeetTag>;
constexpr Feet toFeet(Meters m) {
return Feet(m.get() * 3.28084);
}
constexpr auto height = toFeet(Meters(1.8));
static_assert(height.get() > 5.9); // verified at compile time
Compile-Time String Processing
constexpr bool isValidEmail(const char* s) {
bool hasAt = false;
bool hasDot = false;
int atPos = -1;
int len = 0;
for (int i = 0; s[i] != '\0'; ++i) {
if (s[i] == '@') { hasAt = true; atPos = i; }
if (s[i] == '.' && atPos > 0 && i > atPos) hasDot = true;
++len;
}
return hasAt && hasDot && atPos > 0 && atPos < len - 2;
}
static_assert(isValidEmail("user@example.com"));
static_assert(!isValidEmail("invalid-email"));
9. Practice Exercises
Exercise 1: Compile-Time FizzBuzz
Write a constexpr function that determines the FizzBuzz classification (fizz, buzz, fizzbuzz, number) for any integer. Verify with static_assert.
Exercise 2: constexpr Matrix
Implement a small constexpr Matrix class with addition, multiplication, and determinant — all computable at compile time.
Exercise 3: Compile-Time Parser
Write a constexpr function that parses a simple expression like “3+4*2” and returns the result, evaluated at compile time.
What’s Next?
You’ve mastered compile-time computing! Next up is std::optional, variant & any (C++17) — type-safe unions and nullable types that replace error-prone C patterns.
Return to the C++ Learning Roadmap to continue your journey.