Understanding constexpr, consteval, and constinit in Modern C++

C++ has always supported compile-time constants through macros and const. However, with the evolution of the language from C++11 to C++20, new keywords have been introduced to enable better compile-time computation, safety, and performance:

  • constexpr (C++11, enhanced in C++14 and C++20)
  • consteval (C++20)
  • constinit (C++20)

We will explore the differences, use-cases, and examples of these three powerful keywords.

constexpr – Compile-Time Evaluated Expressions

What is constexpr?

constexpr indicates that a variable or function can be evaluated at compile time if its arguments are also constexpr.

  • Introduced in C++11 for constants and simple functions
  • C++14 allowed more complex bodies, control structures (if, for)
  • C++20 added support for dynamic memory (new, delete) inside constexpr

When to Use

Use constexpr when:

  • You want a value or function to be usable at compile-time
  • But you still allow fallback to run-time if needed
constexpr Function Example
constexpr int square(int x) {
    return x * x;
}

int main() {
    constexpr int x = square(5); // evaluated at compile-time
    int y = square(10);          // evaluated at run-time
}

constexpr Variable Example
constexpr double pi = 3.14159;
constexpr double area(double r) {
    return pi * r * r;
}

User-defined types can use constexpr:

struct Point {
    int x, y;
    constexpr Point(int x, int y) : x(x), y(y) {}
};

constexpr Point p1(10, 20); // OK: evaluated at compile-time

consteval – Immediate Compile-Time Evaluation

What is consteval?

consteval functions must be evaluated at compile-time. They cannot be called at run-time.

  • The function is meant only for compile-time constant generation
  • Any attempt to call it at runtime should be a compilation error
consteval Example
consteval int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int f = factorial(5);  //OK
    int x = 5;
    // int f2 = factorial(x);        //Error: x not constexpr
}

constinit – Guaranteed Static Initialization

What is constinit?

constinit ensures a variable is statically initialized at compile time, not dynamically at runtime.

  • Introduced in C++20
  • Useful to avoid the Static Initialization Order Fiasco
When to Use

Use constinit when:

  • You want compile-time initialization
  • But the variable is not constexpr
  • The value might change later (so constexpr cannot be used)
constinit Example
constinit int global_value = 100; // initialized at compile-time

void update() {
    global_value = 200; //allowed: not constexpr
}
Real-world Example: Static Initialization Order Fiasco
#include <iostream>

constinit int globalA = 42;

int getA() {
    return globalA;
}

int main() {
    std::cout << getA() << "\n"; // Safe, globalA is constinit
}

Without constinit, globalA might get dynamically initialized after getA() is called — leading to undefined behavior.

Conclusion

  • Use constexpr for general-purpose compile-time constructs.
  • Use consteval when you want enforced compile-time logic.
  • Use constinit to avoid static initialization order issues without forcing immutability.