C++ constexpr consteval compile-time computing tutorial
|

C++ constexpr & Compile-Time Computing: consteval, if constexpr 2026

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *