constexpr and consteval — Compile-Time Evaluation, Constant Expressions, Immediate Functions
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
- 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.
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.
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.
Write a constexpr function that reverses a string.
constexpr std::string_view reverse(std::string_view s) {
// Requires compile-time allocation or fixed buffer
}
- 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
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