Constexpr#
The constexpr keyword, introduced in C++11, enables compile-time evaluation
of expressions and functions. By shifting computation from runtime to compile
time, constexpr improves performance, enables stronger type checking, and
allows values to be used in contexts requiring compile-time constants (such as
array sizes and template arguments). C++14, C++17, and C++20 progressively
relaxed restrictions, making constexpr increasingly powerful. Related
keywords consteval (C++20) and constinit (C++20) provide finer control
over when evaluation and initialization must occur.
constexpr Functions#
- Source:
The constexpr specifier declares that a function can be evaluated at
compile time when called with constant expressions. This enables the compiler
to compute results during compilation, eliminating runtime overhead entirely.
When called with non-constant arguments, the function executes at runtime
like a normal function. This dual behavior makes constexpr functions
versatile for both compile-time and runtime use.
#include <chrono>
#include <iostream>
constexpr long fib(long n) {
return (n < 2) ? n : fib(n - 1) + fib(n - 2);
}
int main() {
// Runtime evaluation
auto start = std::chrono::system_clock::now();
long r1 = fib(40);
std::chrono::duration<double> d1 = std::chrono::system_clock::now() - start;
std::cout << "Runtime: " << d1.count() << "s\n";
// Compile-time evaluation
start = std::chrono::system_clock::now();
constexpr long r2 = fib(40);
std::chrono::duration<double> d2 = std::chrono::system_clock::now() - start;
std::cout << "Compile-time: " << d2.count() << "s\n";
}
$ g++ -std=c++17 -O3 a.cpp && ./a.out
Runtime: 0.268229s
Compile-time: 8e-06s
constexpr vs Template Metaprogramming#
- Source:
Before C++11, compile-time computation required template metaprogramming (TMP),
which uses recursive template instantiation. While powerful, TMP is verbose
and difficult to read. The constexpr keyword provides a cleaner alternative
that looks like regular code. Both approaches achieve compile-time evaluation,
but constexpr is more maintainable and easier to debug.
#include <iostream>
// Template metaprogramming approach (pre-C++11 style)
template <long N>
struct Fib {
static constexpr long value = Fib<N - 1>::value + Fib<N - 2>::value;
};
template <>
struct Fib<0> {
static constexpr long value = 0;
};
template <>
struct Fib<1> {
static constexpr long value = 1;
};
// constexpr approach (modern C++)
constexpr long fib(long n) {
return (n < 2) ? n : fib(n - 1) + fib(n - 2);
}
int main() {
constexpr long r1 = Fib<40>::value; // TMP
constexpr long r2 = fib(40); // constexpr
std::cout << r1 << " " << r2 << "\n"; // Both: 102334155
}
constexpr Variables#
- Source:
A constexpr variable must be initialized with a constant expression and
its value is fixed at compile time. Unlike const, which only promises
the variable won’t be modified after initialization, constexpr guarantees
the value is known at compile time. This makes constexpr variables usable
in contexts requiring compile-time constants, such as array sizes and template
arguments.
#include <array>
#include <iostream>
constexpr int square(int x) { return x * x; }
int main() {
constexpr int size = square(4); // Computed at compile time: 16
std::array<int, size> arr{}; // Array size must be compile-time constant
std::cout << arr.size() << "\n"; // Output: 16
const int runtime_val = size; // const: won't change, but could be runtime
constexpr int compile_val = size; // constexpr: guaranteed compile-time
}
constexpr if (C++17)#
- Source:
C++17 introduced if constexpr, which evaluates conditions at compile time
and discards the untaken branch entirely. This is essential for template
metaprogramming because it prevents compilation errors in branches that would
be invalid for certain template instantiations. Unlike regular if, both
branches of a normal if must be valid code even if one is never executed.
#include <iostream>
#include <type_traits>
template <typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // Only compiled when T is a pointer
} else {
return t; // Only compiled when T is not a pointer
}
}
int main() {
int x = 42;
int *p = &x;
std::cout << get_value(x) << "\n"; // Output: 42
std::cout << get_value(p) << "\n"; // Output: 42
}
if consteval: Detecting Constant Evaluation (C++23)#
- Source:
C++23 introduced if consteval to detect whether code is executing during
constant evaluation. This allows a function to choose different implementations
for compile-time versus runtime execution. Unlike if constexpr, which
evaluates a compile-time boolean condition, if consteval checks the
evaluation context itself. This is useful when compile-time evaluation requires
a different algorithm (e.g., a safer but slower approach) than runtime execution.
#include <iostream>
constexpr int compute(int x) {
if consteval {
// Compile-time path: safe but potentially slower
return x * x;
} else {
// Runtime path: can use optimized or platform-specific code
return x * x; // In practice, might call intrinsics here
}
}
int main() {
constexpr int a = compute(5); // Uses compile-time path
int b = compute(5); // Uses runtime path
std::cout << a << " " << b << "\n"; // Output: 25 25
}
Negated form (if !consteval):
constexpr int optimized(int x) {
if !consteval {
// Runtime-only optimizations
return x * x;
}
// Compile-time fallback
return x * x;
}
consteval: Immediate Functions (C++20)#
- Source:
C++20 introduced consteval to declare immediate functions that must be
evaluated at compile time. Unlike constexpr, which allows runtime evaluation
when arguments are not constant, consteval functions produce a compilation
error if called with non-constant arguments. This guarantees that the function
never generates runtime code, making it useful for compile-time-only utilities
like string hashing or lookup table generation.
#include <iostream>
consteval int square(int x) { return x * x; }
int main() {
constexpr int a = square(4); // OK: compile-time evaluation
std::cout << a << "\n"; // Output: 16
// int b = 5;
// int c = square(b); // Error: b is not a constant expression
}
Comparison of constexpr vs consteval:
constexpr int ce_square(int x) { return x * x; }
consteval int cv_square(int x) { return x * x; }
int main() {
int runtime_val = 5;
int a = ce_square(runtime_val); // OK: runtime evaluation
// int b = cv_square(runtime_val); // Error: must be compile-time
constexpr int c = ce_square(5); // OK: compile-time
constexpr int d = cv_square(5); // OK: compile-time
}
constinit: Constant Initialization (C++20)#
- Source:
C++20 also introduced constinit to ensure a variable is initialized at
compile time, avoiding the static initialization order fiasco. Unlike
constexpr, a constinit variable is not const and can be modified at
runtime. It only guarantees that initialization happens at compile time,
preventing issues where static variables in different translation units
depend on each other’s initialization order.
#include <iostream>
constexpr int compute() { return 42; }
constinit int global = compute(); // Initialized at compile time
int main() {
std::cout << global << "\n"; // Output: 42
global = 100; // OK: constinit doesn't mean const
std::cout << global << "\n"; // Output: 100
}
constexpr Evolution by Standard#
C++11:
Introduced
constexprfor simple functions (single return statement)constexprvariables must be initialized with constant expressions
C++14:
constexprfunctions can have multiple statements, loops, and local variablesconstexprmember functions are no longer implicitlyconst
C++17:
if constexprfor compile-time conditional compilationconstexprlambdas
C++20:
constevalfor immediate functions (must evaluate at compile time)constinitfor compile-time initialization of non-const variablesconstexprvirtual functionsconstexprdynamic allocation (new/delete) within constant expressionsconstexprstd::vectorandstd::string
C++23:
if constevalfor detecting constant evaluation contextRelaxed
constexprrestrictions on non-literal types