C++ constexpr Functions: Compile-Time Evaluation Rules from C++11 to C++17
Key takeaways
C++ constexpr functions: compile-time and runtime use, C++11 vs C++14 vs C++17, arrays, classes, and optimization. Practical examples and pitfalls.
What is a constexpr function?
A function that can be used in compile-time and runtime contexts.
The keyword itself is a request, not a guarantee — constexpr on a function means “this function may be evaluated at compile time if its arguments are constant expressions,” not “this function always runs at compile time.” That single fact resolves most of the confusion people have around constexpr: the exact same function definition serves both purposes, and the compiler decides which mode applies at each call site based purely on whether the arguments happen to be known at compile time. This is fundamentally different from a macro or a hand-written compile-time-only metaprogramming trick, which has no runtime fallback at all — constexpr functions are ordinary functions first, with compile-time evaluation as an optional capability layered on top.
constexpr int square(int x) {
return x * x;
}
int main() {
// compile time
constexpr int a = square(5); // 25
// runtime
int x = 10;
int b = square(x); // runtime evaluation
}
C++11 vs C++14 vs C++17
The evolution across standards is really the story of constexpr being relaxed from an extremely narrow, functional-programming-style restriction into something you can write like ordinary imperative code. C++11 required a constexpr function body to be a single return statement (plus a limited set of declarations), which is why factorial11 above is forced into a recursive ternary rather than a loop — there was no other way to express conditional logic in a single expression. C++14 dropped that restriction almost entirely: local variables, loops, multiple statements, and even multiple return points became legal, which is why factorial14 can use a plain for loop instead of recursion. In practice, this means recursive constexpr functions written in the C++11 style are now mostly a historical artifact — write them as ordinary loops under C++14 or later unless you specifically need C++11 compatibility, since deep unbounded recursion can also hit the compiler’s constexpr evaluation step limit faster than an equivalent loop. C++17’s contribution, if constexpr, solves a different problem entirely: it’s compile-time branching within a template, discarding the untaken branch before instantiation so that process<T> can compile for types where only one branch would even type-check — ordinary runtime if can’t do this because both branches must compile for every instantiation.
// C++11: often a single return statement
constexpr int factorial11(int n) {
return n <= 1 ? 1 : n * factorial11(n - 1);
}
// C++14: multiple statements, loops
constexpr int factorial14(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// C++17: if constexpr for templates
template<typename T>
constexpr auto process(T value) {
if constexpr (std::is_integral_v<T>) {
return value * 2;
} else {
return value;
}
}
Where constexpr Becomes Load-Bearing
This is the case where constexpr stops being a nice-to-have and becomes load-bearing: int arr[SIZE] requires SIZE to be a compile-time constant, because a plain C-style array’s size is part of its type and must be known when the compiler generates code for it. A regular const int SIZE = add(10, 20) would compile the call to add at runtime (even though the compiler might optimize it away), and using that as an array bound would be a compile error in standard C++ — only a genuine constant expression, which constexpr guarantees when the arguments are themselves constant expressions, satisfies that requirement. This is the same mechanism that lets constexpr values feed into template non-type parameters, static_assert conditions, and switch case labels — anywhere the language specifically demands a value the compiler can resolve before code generation.
constexpr int add(int a, int b) {
return a + b;
}
constexpr int multiply(int a, int b) {
return a * b;
}
constexpr int SIZE = add(10, 20);
int arr[SIZE]; // compile-time array size
Math, String, Hash, and Array Helpers
The four examples below aren’t just toy demonstrations — they represent the actual categories of work constexpr functions get used for in real codebases: compile-time-derivable numeric constants, compile-time string inspection (useful for validating format strings or configuration keys at build time), lookup-table generation, and compile-time hashing (often used for switch-based string dispatch, since C++ doesn’t allow switch on std::string directly).
Example 1: Math helpers
constexpr int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
constexpr int power(int base, int exp) {
int result = 1;
for (int i = 0; i < exp; i++) {
result *= base;
}
return result;
}
constexpr bool isPrime(int n) {
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
int main() {
constexpr int fib10 = fibonacci(10);
constexpr int pow = power(2, 10);
constexpr bool prime = isPrime(17);
std::cout << fib10 << '\n' << pow << '\n' << prime << '\n';
}
Recursive fibonacci here is a useful illustration of a real pitfall: naive recursive Fibonacci is exponential-time regardless of when it runs, and while a small fibonacci(10) compiles instantly, a poorly bounded compile-time recursive computation (say, fibonacci(35)) can make build times balloon or hit the compiler’s implementation-defined constexpr evaluation-step limit (-fconstexpr-steps on Clang, /constexpr:steps on MSVC) — a category of problem that has no runtime analog, since a slow runtime function just runs slow, but a slow compile-time function makes every build slow. isPrime is a good candidate for genuine compile-time use (validating a constexpr configuration value, say), precisely because it’s linear rather than exponential.
Example 2: String helpers
C string literals are constexpr-friendly because their length and contents are fixed at compile time, which lets functions like strlen_constexpr compute a length without any runtime string-scanning cost when called with a literal — the equivalent of std::strlen still has to walk the buffer at runtime even for a literal argument, since the standard library function isn’t constexpr. This is a genuinely useful pattern for embedded and performance-sensitive code: sizing a char buffer[len + 1] from a compile-time-known literal length costs nothing at runtime, whereas the equivalent computed with a runtime strlen call would.
constexpr size_t strlen_constexpr(const char* str) {
size_t len = 0;
while (str[len] != '\0') {
len++;
}
return len;
}
constexpr bool startsWith(const char* str, const char* prefix) {
while (*prefix != '\0') {
if (*str != *prefix) return false;
str++;
prefix++;
}
return true;
}
int main() {
constexpr size_t len = strlen_constexpr("Hello");
constexpr bool starts = startsWith("Hello World", "Hello");
char buffer[len + 1];
}
Example 3: Hash (illustrative)
This is the classic djb2 hash, marked “illustrative” for good reason: computing it as constexpr lets you hash a string literal at compile time and use the result in a switch statement (switch (hash(key)) { case hash("start"): ... }), which is the usual workaround for C++‘s lack of native switch-on-std::string. The caveat worth flagging explicitly: this is a hash-collision risk, not a string-equality check — two different strings can (rarely) produce the same 32-bit hash, so any production use of this pattern needs a follow-up == comparison against the literal after the switch dispatches to the matching case, not blind trust that a hash match implies a string match.
constexpr unsigned int hash(const char* str) {
unsigned int h = 5381;
while (*str) {
h = ((h << 5) + h) + *str;
str++;
}
return h;
}
Example 4: Array generation
Generating a lookup table this way trades a small amount of compile time for zero runtime cost and zero risk of the table being computed incorrectly at startup (a common bug with runtime-initialized static lookup tables, which can run into static-initialization-order issues across translation units). Because std::array<int, N> has its size fixed at compile time and generateArray is a template on N, the entire table — all 10 values — is baked directly into the binary as constant data; nothing runs when main starts beyond reading already-computed values. This pattern shows up frequently for things like precomputed sine/cosine tables, CRC lookup tables, and small perfect-hash tables where the input domain is known in advance.
constexpr int generateValue(int index) {
return index * index;
}
template<size_t N>
constexpr std::array<int, N> generateArray() {
std::array<int, N> arr{};
for (size_t i = 0; i < N; i++) {
arr[i] = generateValue(i);
}
return arr;
}
int main() {
constexpr auto arr = generateArray<10>();
for (int x : arr) {
std::cout << x << " ";
}
}
constexpr classes
A constexpr constructor and constexpr member functions let an entire object — not just a scalar value — exist as a compile-time constant. This matters for exactly the same reason scalar constexpr matters: constexpr Point p(3, 4) followed by constexpr int dist = p.distanceSquared() means the object construction, the member function call, and the arithmetic inside it all happen at compile time, and dist can then be used anywhere a constant expression is required (like the array bound arr[dist] below). The class itself needs no special “constexpr” declaration beyond marking the relevant constructor and member functions — any member function marked constexpr is implicitly usable at compile time as long as the object it’s called on is itself a constant expression, and implicitly falls back to ordinary runtime execution for non-constant objects, following the same dual-mode behavior as free functions.
class Point {
int x, y;
public:
constexpr Point(int x, int y) : x(x), y(y) {}
constexpr int getX() const { return x; }
constexpr int getY() const { return y; }
constexpr int distanceSquared() const { return x * x + y * y; }
};
int main() {
constexpr Point p(3, 4);
constexpr int dist = p.distanceSquared();
int arr[dist];
}
Why a Function Cannot Be constexpr
Calling non-constexpr functions
What happens with bad below depends on the standard version, and this is a place where tutorials often disagree with the compiler in front of you.
int nonConstexpr(int x) { return x * 2; }
constexpr int bad(int x) {
return nonConstexpr(x); // GCC 10, -std=c++20: error: call to non-'constexpr' function
}
constexpr int good(int x) { return x * 2; }
constexpr int ok(int x) { return good(x); }
Up to C++20, a constexpr function for which no set of arguments could ever produce a constant expression is “ill-formed, no diagnostic required”. Compilers are allowed to reject it at the definition, and GCC does: the snippet above fails with error: call to non-'constexpr' function 'int nonConstexpr(int)' even if bad is only ever called at runtime. Other compilers may accept it and only complain at a call site that needs a constant. C++23 (P2448) removed the requirement, so newer compilers in C++23 mode accept the definition and report the error only where compile-time evaluation is actually demanded. If code has to build on several compilers or standard versions, do not rely on the lenient behavior.
static variables inside constexpr functions
constexpr int counter() {
static int count = 0; // GCC 10, -std=c++20: error: 'count' declared 'static' in 'constexpr' function
return count++;
}
This is sometimes described as “allowed since C++14”, which is wrong. C++14 relaxed loops, local variables and multiple statements, but static and thread_local variables stayed forbidden through C++20. C++23 (P2647) allows static constexpr locals, and P2242 allows other statics to appear in a constexpr function as long as they are not reached during constant evaluation. A function that mutates a static counter can still never be evaluated at compile time — constant evaluation has no persistent state.
Undefined behavior is a compile error
constexpr int next(int x) { return x + 1; }
constexpr int k = next(2147483647); // error: overflow in constant expression
At runtime, signed overflow is undefined behavior that may silently wrap or be optimized in surprising ways. During constant evaluation the compiler is required to diagnose it, along with out-of-bounds array access and reads of uninitialized values. That makes static_assert over a constexpr function a cheap way to catch UB in small pieces of logic: the same function that “works” at runtime can refuse to compile when evaluated with the edge-case input.
Virtual functions
Before C++20, a virtual function could not be constexpr. Since C++20 (P1064) it can, and a virtual call is allowed during constant evaluation when the dynamic type of the object is known at that point, for example on a constexpr object or one created inside the constant evaluation. Overriders may be constexpr or not; a non-constexpr overrider simply cannot be called at compile time.
Runtime-only values
This is the flip side of the entire feature, and the case most likely to trip someone reading constexpr for the first time: square(runtime_value) compiles and runs completely normally, because constexpr never forbids runtime execution — it only makes compile-time execution possible when the inputs allow it. There’s no separate “runtime version” of the function generated anywhere; it’s the exact same function body, and the compiler chooses at each call site whether it can fold the call into a constant or must emit a normal function call.
constexpr int square(int x) { return x * x; }
int main() {
int runtime_value;
std::cin >> runtime_value;
int result = square(runtime_value); // OK at runtime
}
constexpr vs const
The two keywords answer different questions and are easy to conflate because both prevent modification after initialization. const only promises “this won’t change after it’s initialized” — the initialization itself can happen at runtime, as getValue() demonstrates, so const alone gives no compile-time guarantee whatsoever. constexpr promises something strictly stronger: “this value is known at compile time,” which necessarily implies it can’t change afterward, so every constexpr variable is also implicitly const, but not every const variable is constexpr. The practical rule: reach for constexpr whenever you specifically need the compile-time guarantee (array bounds, template arguments, values that must fold into constants), and reach for plain const when you only need immutability and the value might legitimately come from a runtime source like a config file or user input.
Does constexpr Code Actually Fold into Constants?
Heavy constexpr computation can fold entirely into constants in the object file, but whether that actually happens for a runtime call to a constexpr function is implementation-defined, not guaranteed by the standard — the compiler is free to still emit a real function call for square(runtime_value) if it chooses not to inline and constant-fold, even though it’s permitted to optimize it away entirely. The one place the standard forces compile-time evaluation is a genuine constant-expression context (array bounds, static_assert, template arguments) — everywhere else, constexpr is a guarantee that compile-time evaluation is possible, and actual compile-time folding for ordinary calls is a quality-of-implementation optimization you can observe by checking generated assembly, but shouldn’t structure your program’s correctness around.
FAQ
Q: When to use constexpr? For compile-time constants, sizes, and generic code that must run at compile time when possible. Q: Runtime use? Yes—constexpr functions are still normal functions when inputs are not constant expressions. Q: Learning resources: Effective Modern C++, cppreference, C++ Templates: The Complete Guide.