Skip to content

constexpr and consteval — Compile-Time Evaluation, Constant Expressions, Immediate Functions

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about constexpr and consteval. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ constexpr and consteval enable functions and variables to be evaluated at compile time, transforming runtime computation into compile-time constants with guaranteed zero runtime cost.

What You'll Learn

You will write constexpr functions that work at both compile and runtime, use consteval for immediate functions that must run at compile time, understand what operations are allowed in constant expressions, build compile-time data structures and lookup tables, and use constexpr for template Metaprogramming and type traits.

Why It Matters

Compile-time computation eliminates runtime overhead for calculations that only depend on known inputs. A CRC32 hash, a lookup table, or a regex parser can run entirely at compile time, producing zero-cost constants. C++ compilers evaluate millions of constexpr computations per second, making them practical for real-world libraries and performance-critical code.

Learning Path

graph LR
    A["46: SFINAE & enable_if"] --> B["47: constexpr & consteval"]
    B --> C["48: Concepts & Requires"]
    C --> D["49: Type Traits & Metaprogramming"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic constexpr Functions

A constexpr function can be evaluated at compile time when called with constant arguments, or at runtime otherwise.

#include <iostream>

// C++14: constexpr functions can have loops and local variables
constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

// C++11 style: recursive only (no loops)
constexpr int factorial11(int n) {
    return n <= 1 ? 1 : n * factorial11(n - 1);
}

int main() {
    // Compile-time evaluation
    constexpr int fact10 = factorial(10);
    std::cout << fact10 << "\n";  // 3628800

    // Also works at runtime
    int n = 5;
    std::cout << factorial(n) << "\n";  // 120 (runtime)

    // Template argument (must be compile-time)
    std::array<int, factorial(4)> arr;  // array of 24 ints
    std::cout << arr.size() << "\n";    // 24
}

A constexpr function does not guarantee compile-time evaluation. It guarantees possible compile-time evaluation. Use constexpr variables or template arguments to force it.

constexpr Variables

Variables declared constexpr must be initialized with a constant expression.

#include <iostream>
#include <array>
#include <cmath>

constexpr double PI = 3.141592653589793;
constexpr double DEG_TO_RAD = PI / 180.0;

constexpr double degToRad(double deg) {
    return deg * DEG_TO_RAD;
}

// constexpr array of values (C++17)
constexpr std::array<double, 5> sinValues = {
    std::sin(degToRad(0)),
    std::sin(degToRad(30)),
    std::sin(degToRad(45)),
    std::sin(degToRad(60)),
    std::sin(degToRad(90))
};

int main() {
    for (double val : sinValues) {
        std::cout << val << " ";
    }
    std::cout << "\n";
    // 0 0.5 0.707107 0.866025 1
}

The sinValues array is computed entirely at compile time. No runtime trigonometric calls occur.

constexpr in C++14 vs C++11

C++14 significantly relaxed constexpr restrictions:

#include <iostream>

// C++11 constexpr: single return statement, no side effects
constexpr int square11(int x) {
    return x * x;
}

// C++14 constexpr: loops, local variables, mutation, if/switch
constexpr int sumSquares(int n) {
    int total = 0;
    for (int i = 1; i <= n; ++i) {
        total += i * i;
    }
    return total;
}

int main() {
    constexpr int s1 = square11(5);    // 25
    constexpr int s2 = sumSquares(5);  // 55
    std::cout << s1 << " " << s2 << "\n";
}

C++17 further allowed lambda expressions in constexpr context. C++20 added dynamic allocation, try/catch, virtual calls, and typeid in constexpr.

consteval — Immediate Functions (C++20)

consteval forces compile-time evaluation. If called with runtime arguments, compilation fails.

#include <iostream>
#include <cassert>

// Must be evaluated at compile time
consteval unsigned long long hash(const char* str) {
    unsigned long long h = 14695981039346656037ULL;
    for (const char* p = str; *p; ++p) {
        h ^= static_cast<unsigned long long>(*p);
        h *= 1099511628211ULL;
    }
    return h;
}

// Compile-time string hashing
constexpr unsigned long long HASH_FOO = hash("foo");
constexpr unsigned long long HASH_BAR = hash("bar");

void handle(const char* input, unsigned long long expected) {
    if (hash(input) != expected) {
        // runtime hash is fine here since input is not constant
    }
}

int main() {
    std::cout << "hash(foo) = " << HASH_FOO << "\n";
    std::cout << "hash(bar) = " << HASH_BAR << "\n";

    // hash(input);  // Error: input is not a constant expression
}

Use consteval when the function has no meaningful runtime semantics (like compile-time hashes, type computations, or Code Generation).

constexpr Class Types

User-defined types can be constexpr, enabling compile-time data structures.

#include <iostream>

class Complex {
    double re_, im_;
public:
    constexpr Complex(double r = 0, double i = 0) : re_(r), im_(i) {}

    constexpr double real() const { return re_; }
    constexpr double imag() const { return im_; }

    constexpr Complex operator+(const Complex& other) const {
        return Complex(re_ + other.re_, im_ + other.im_);
    }

    constexpr Complex operator*(const Complex& other) const {
        return Complex(
            re_ * other.re_ - im_ * other.im_,
            re_ * other.im_ + im_ * other.re_
        );
    }
};

int main() {
    // All computed at compile time
    constexpr Complex i(0, 1);
    constexpr Complex z1(1, 2);
    constexpr Complex z2(3, 4);
    constexpr Complex sum = z1 + z2;
    constexpr Complex prod = z1 * z2;
    constexpr Complex i_squared = i * i;

    std::cout << "i^2 = " << i_squared.real() << " + "
              << i_squared.imag() << "i\n";  // i^2 = -1 + 0i

    std::cout << "Sum = " << sum.real() << " + "
              << sum.imag() << "i\n";         // Sum = 4 + 6i
}

Compile-Time Lookup Tables

Generate complex tables at compile time for zero-cost runtime access.

#include <iostream>
#include <array>
#include <cmath>

// Compile-time sine lookup table
template <size_t N>
class SinTable {
    std::array<double, N> values_;
public:
    constexpr SinTable() : values_{} {
        for (size_t i = 0; i < N; ++i) {
            values_[i] = std::sin(2.0 * PI * i / N);
        }
    }
    constexpr double operator[](size_t i) const { return values_[i]; }
    constexpr size_t size() const { return N; }

    static constexpr double PI = 3.141592653589793;
};

int main() {
    // 1024-entry sine table, computed at compile time
    constexpr SinTable<1024> sineTable;

    // Zero-cost runtime access
    for (size_t i = 0; i < 10; ++i) {
        std::cout << sineTable[i] << " ";
    }
    std::cout << "\n";
}

This pattern is used in game engines, audio processing, and scientific computing where lookup tables replace expensive runtime computations.

constexpr and if constexpr

Combining constexpr evaluation with if constexpr creates powerful compile-time logic.

#include <iostream>
#include <type_traits>

// Compile-time power function
template <typename T>
constexpr T power(T base, int exp) {
    if constexpr (std::is_floating_point_v<T> && exp < 0) {
        return 1.0 / power(base, -exp);
    } else {
        T result = 1;
        for (int i = 0; i < exp; ++i) {
            result *= base;
        }
        return result;
    }
}

int main() {
    constexpr auto p1 = power(2, 10);            // 1024
    constexpr auto p2 = power(2.0, -2);          // 0.25
    constexpr auto p3 = power(3.0f, 3);          // 27.0f

    std::cout << p1 << " " << p2 << " " << p3 << "\n";

    // Verify types
    static_assert(std::is_same_v<decltype(p1), const int>);
    static_assert(std::is_same_v<decltype(p2), const double>);
}

constexpr Limitations (C++20)

C++20 constexpr allows almost everything, but not:

// Not allowed in anything before C++26:
// - static_assert with non-constant message
// - std::vector (allocations allowed in C++20 at compile time, but tricky)
// - file I/O
// - random (unless using deterministic PRNG)
// - std::chrono::system_clock (before C++20)

// C++20 allows:
// - dynamic allocation (must be deallocated within evaluation)
// - try/catch
// - virtual calls (if object is constant)
// - typeid
// - placement new

Common Mistakes

Mistake 1: Assuming constexpr guarantees compile-time

constexpr int add(int a, int b) { return a + b; }
int x = add(std::rand() % 10, std::rand() % 10);  // Runtime: OK

add runs at runtime here because arguments are not constant.

Mistake 2: Using non-constexpr functions inside constexpr

constexpr double bad(double x) {
    return std::log(x);  // Error in C++11-17 (std::log not constexpr)
}

Check whether library functions are marked constexpr. C++23 makes most math functions constexpr.

Mistake 3: Modifying static or thread_local variables

constexpr int foo() {
    static int x = 0;  // Error: static in constexpr context
    return x;
}

Mistake 4: Forgetting that constexpr implies const for variables

constexpr int x = 5;
x = 6;  // Error: constexpr variables are const

Mistake 5: Overestimating compile-time capabilities

constexpr void parse(std::string_view input) {
    // Reads characters, builds structures — OK
    // Opens files, reads network — NOT OK
}

Practice Questions

  1. What is the output?
constexpr int add(int a, int b) { return a + b; }
int main() { std::cout << add(3, 4); }

Answer: 7 — can be compile or runtime, cannot tell which without enabling compiler output.

  1. What is the difference between constexpr and consteval? Answer: constexpr functions can run at compile or runtime. consteval (C++20) functions MUST run at compile time — calling them with non-constant arguments is a compilation error.

  2. Can constexpr functions throw exceptions (C++20)? Answer: Yes, C++20 allows try/catch in constexpr functions, but a throw at compile time causes a compilation error.

  3. Write a constexpr function that reverses a string.

constexpr std::string_view reverse(std::string_view s) {
    // Requires compile-time allocation or fixed buffer
}
  1. When would you use consteval instead of constexpr? Answer: When the function has no meaningful runtime use (compile-time hashes, type computations) to prevent accidental runtime evaluation.

FAQ

What is the difference between const and constexpr

const guarantees the variable is not modified. constexpr guarantees the value is a compile-time constant. All constexpr variables are const, but not all const variables are constexpr.

Can I use std::vector in constexpr context

C++20 allows dynamic allocation in constexpr, but the allocations must all be freed within the evaluation. std::vector works in constexpr in C++20 with limitations.

What are the C++20 constexpr improvements

Dynamic allocation, try/catch, virtual calls, typeid, and placement new are all allowed in constexpr functions in C++20.

Does constexpr guarantee performance improvement

Not always. Constexpr forces compile-time computation, which increases build time. For small expressions, runtime evaluation may be equally fast. Use constexpr for correctness and for genuinely expensive compile-time computations.

How do I debug constexpr functions

Breakpoints do not work at compile time. Use static_assert to verify constexpr results, split logic into smaller functions, and test with runtime arguments first.

Mini Project

Build a constexpr compile-time regular expression matcher (simple wildcard: * matches anything, ? matches one character):

#include <iostream>

// Your constexpr wildcard matcher
constexpr bool match(const char* pattern, const char* text) {
    // Implement wildcard matching
}

int main() {
    static_assert(match("hello", "hello"));        // true
    static_assert(match("h*", "hello world"));     // true
    static_assert(match("h?llo", "hxllo"));        // true
    static_assert(match("h?llo", "h ello"));       // true (? matches space)
    static_assert(!match("h?llo", "helllo"));      // false (too long)

    constexpr bool test = match("*world", "hello world");
    std::cout << std::boolalpha << test << "\n";   // true

    std::cout << "All compile-time checks passed!\n";
}

This project demonstrates how C++ compile-time evaluation powers libraries like ctre (compile-time regular expressions) and fmt (compile-time format string checking).

What's Next

You now compute at compile time with constexpr and consteval. Next, you will learn C++20 concepts — readable, compiler-checked constraints on template parameters that replace SFINAE boilerplate.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro