constexpr on a function tells the compiler that the function can run during compilation when its arguments are themselves compile-time constants. That is a different feature from constexpr on a variable, which the Constants & Literals lesson covered briefly. This lesson goes deep on constexpr functions: how they evolved from a single-line trick in C++11 into a real subset of the language that can allocate memory in C++20, and when the keyword actually buys you compile-time evaluation versus when it sits there decorating the source.
constexpr Functions ExistC++ has always treated "values known at compile time" specially. Array sizes, template arguments, switch labels, static_assert conditions, default template parameters, non-type template arguments: all of these require a value the compiler can read while building the program, not while running it. Before C++11, the only way to get one of those values was to write a literal (5), a const integer (const int N = 5;), or a hideous template-recursive metafunction.
That last category was painful. A "compile-time factorial" used to look like this:
That works. It also requires a template specialization for the base case, an ::value member, and a reader who knows template metaprogramming. C++11 added constexpr on functions specifically to kill this pattern. The same compile-time factorial, written as an ordinary function:
The function reads like an ordinary one, but the constexpr keyword promises the compiler "this body is simple enough that you can evaluate me at compile time when my inputs are constants." When the call site is factorial(5), the compiler computes 120 while building the program and the runtime never sees the call. When the call site is factorial(userInput), the compiler cannot evaluate it at compile time, so the same function runs as an ordinary function during execution.
That dual nature is the point. A constexpr function is one function definition that serves two roles: a compile-time computation when callers can supply constants, and a regular runtime function when they cannot.
Zero runtime cost when the call is evaluated at compile time. The compiled binary contains only the resulting value, not the call. When the call has to run at runtime (because an argument is not a constant expression), it costs exactly what a normal function call costs. constexpr itself does not speed up or slow down anything.
constexpr looks like a single feature, but it has expanded substantially over four standard revisions. The rules a function body must follow to qualify are much looser today than they were in C++11, and a body that compiles fine under constexpr in C++20 would have been rejected in C++11.
The timeline shows the headline change at each standard. The rules got steadily friendlier, which means modern constexpr code looks almost like normal C++.
In C++11, a constexpr function body was almost a single expression. You could have type aliases and static_asserts, but the executable part had to reduce to one return statement. No loops, no local variables, no mutation. Recursion was the only way to express anything iterative, which is why the factorial above uses the ternary operator.
A C++11 constexpr total-with-tax calculator looked like this:
One line, one return. Anything more complicated had to be written with the ternary operator or split into smaller functions. That restriction is why C++11-era code uses recursion for things normally written as a loop.
C++14 relaxed almost every restriction. Inside a constexpr function body you can now declare local variables, mutate them, write for and while loops, use if and switch statements, and have multiple return statements. The function still cannot do I/O, throw exceptions (until C++20), or allocate dynamic memory (until C++20), but the body looks like ordinary code.
A C++14 constexpr cart total written with a loop:
The body has a local variable (total), a range-based for loop, and runtime-looking mutation (total += price). All of it runs at compile time when the call is in a constant-expression context. The compiler effectively unrolls the loop in its head, accumulates the result, and bakes 119.95 into the binary.
The trade-off is that the std::array parameter has to be constexpr-friendly. std::array is a literal type (more on those in a moment), which is why this works. A std::vector parameter would not have compiled here until C++20.
constexpr Lambdas and Wider Library SupportC++17 added two practical improvements: lambdas are implicitly constexpr when their bodies qualify, and a much larger slice of the standard library was annotated constexpr. You also got if constexpr, which is a separate feature. Together, these changes mean you can write things like a compile-time discount selector with very little ceremony:
The lambda is constexpr automatically because its body only does arithmetic. Earlier standards would have rejected the call applyDiscount(99.99, 25.0) in a constant-expression context.
std::array, std::pair, std::tuple, std::optional, and the comparison utilities in <functional> (like std::less) all gained constexpr member functions in C++17. That made compile-time data structures genuinely useful.
C++20 turned constexpr from "advanced trick" into "a way to write almost any function." The big new powers:
constexpr function can now call new and delete, as long as every allocation is paired with a corresponding deallocation before the constant expression ends. This is what makes constexpr std::vector and constexpr std::string work.constexpr. You can build, mutate, and read them at compile time. The catch is that allocations made during constant evaluation must be freed before the result is used; you cannot return a constexpr std::vector from a constexpr function and expect its storage to survive into the runtime program.constexpr functions. The catch block cannot actually catch a thrown exception during constant evaluation (throwing is still rejected), but the body can contain the constructs.constexpr, and the dispatch happens at compile time.constexpr std::vector and friends to clean up.The headline example is computing a sum at compile time using std::vector:
Compile with g++ -std=c++20 file.cpp. The std::vector is created, populated, summed, and destroyed entirely during compilation. The runtime program contains only the integer 11995.
The "destroyed before the constant expression ends" rule is strict. Returning a constexpr std::vector from a constexpr function and using it at the call site does not currently work in C++20, because the vector's heap storage would not survive into the runtime data segment. C++23 begins to lift some of these restrictions, but for now, treat constexpr std::vector as a tool for computing a value (a sum, a count, a hash) rather than producing a container the runtime program holds onto.
constexpr Functions vs constexpr VariablesThe keyword does very different work in those two positions. The Constants & Literals lesson covered constexpr on variables, where the rule is simple: the initializer must be a constant expression, and the variable's value is fixed at compile time. The function case is more nuanced because constexpr on a function is a possibility, not a guarantee.
| Context | What constexpr means | When it is evaluated |
|---|---|---|
constexpr variable | The initializer must be a constant expression. | At compile time, always. |
constexpr function | The function can be evaluated at compile time. | At compile time if the call site requires a constant expression and the arguments are constants. Otherwise at runtime. |
That second row is where most confusion lives. Just because a function is declared constexpr does not mean every call to it runs at compile time. The call site decides:
Sample Output:
The first call has a literal argument, and the result is assigned to a constexpr variable. Both conditions push the compiler to evaluate shippingCost(2.5) at build time. The second call passes a runtime variable, so the same function runs ordinarily during execution. One function, two behaviors, determined by the call site.
const vs constexpr on a Function Return ValueThis causes confusion too. const on a function return value says "the returned value cannot be modified through this reference." constexpr on the function says "this function can run at compile time." They are orthogonal:
The first compiles at compile time when called with a constant. The second is a regular runtime function whose return value happens to be const. The third is both. Mixing the two ideas (compile-time-ness and write-protection) is fine and occasionally useful, but they remain independent.
A constexpr function is allowed to run at compile time. There is no guarantee it will. To get the guarantee, put the call in a context that requires a constant expression. The four common ones:
constexpr int x = factorial(5); forces factorial(5) to be evaluated at compile time.std::array<int, factorial(5)> a; forces evaluation. Same with any other template parameter that expects an integer constant.static_assert(factorial(5) == 120); runs the function at compile time and fails the build if the result is wrong.int arr[factorial(5)]; forces evaluation (in contexts that accept built-in arrays).The simplest, most idiomatic way to force compile-time evaluation is the static_assert pattern. It also doubles as a unit test: if the function ever returns the wrong value, the build fails.
The two static_asserts force applyTax(100.0) and applyTax(0.0) to run at compile time. If you ever change applyTax to use a 7% rate instead of 8%, the build breaks before you get anywhere near a test runner.
The diagram traces what the compiler does at every call. The "compile error" branch on the right is the unexpected one: if the context requires a constant expression but the arguments are not constants, the compiler reports an error, even though the function itself is constexpr. That is why mixing constexpr with runtime inputs requires care about where the call appears.
For a stricter rule, "this function must always run at compile time, never at runtime", C++20 added consteval. Treat consteval as "a constexpr function that refuses to run at runtime", and use it when you specifically do not want the dual-mode behavior.
constexpr ConstructorsFor your own classes to participate in compile-time computation, the type itself has to be a literal type: a type whose constructor, destructor, and member access can all run as part of a constant expression. The rules are technical but the spirit is simple: a literal type is one the compiler can build, manipulate, and tear down without doing anything that has to wait for runtime.
A class becomes a literal type when:
constexpr.constexpr (C++20 and later).A small Product type set up as a literal type, used to assemble a compile-time catalog:
The whole catalog lives in the binary as a constexpr array of three Product objects. The totalInventoryValue function runs at compile time, sums up the valueOnHand() of each product, and reports 3744.5 without a single instruction executing at runtime. The runtime program prints a number that was computed during compilation.
The same with a std::string member would require C++20 (because std::string only became literal-type-friendly in C++20) and would hit the storage-lifetime issue mentioned earlier. For C++17 code, stick to literal types built from arithmetic types, enums, and other literal types.
inlineA constexpr function is implicitly inline. That has nothing to do with "should the compiler inline it" performance; the inline keyword in C++ controls the one-definition rule (ODR), which says a non-inline function defined in a header would cause linker errors if the header is included from multiple translation units. Since constexpr functions usually need to be defined in headers (the compiler needs to see the body to evaluate the call), they are implicitly inline so multiple translation units can share the same definition without ODR violations.
Practically: you can put a constexpr function definition directly in a header file with no inline keyword, include it from twenty .cpp files, and the program will link cleanly. This is the same rule that applies to function templates, and for the same reason.
constexpr is often described as a performance feature. That is only true when the call is actually evaluated at compile time. When it runs at runtime, a constexpr function performs exactly like an ordinary function. The keyword does not make the function faster; it unlocks the ability to run at compile time when conditions permit.
Two performance facts worth remembering:
constexpr function evaluated at compile time costs zero CPU cycles when the program runs, but the build will take longer.constexpr function costs exactly what the same function would cost without `constexpr`. The compiler still applies its usual inlining heuristics. The keyword is irrelevant to runtime performance once the call has been forced to runtime.Heavy use of constexpr shifts work from runtime to build time. That is a great trade for embedded constants, lookup tables, and hash seeds. It is a poor trade for large computations that compile every time you rebuild but only run rarely. Profile if you are not sure.
A pattern that hits both wins: precompute a lookup table at compile time, then index into it at runtime.
Sample Output:
The 11-element table is built at compile time, including the loop and the nested ternary. At runtime, the program does a single array index, which is one of the cheapest operations a CPU can do. If you wrote bulkDiscountTable as an ordinary function and called it on demand, you would pay for the loop on every lookup. The constexpr version pays for it once, at compile time, never at runtime.
Pulling everything together: a small pricing engine for an online store that decides quantity-based discount tiers at compile time, then applies them at runtime to whatever cart the customer brings in.
Sample Output:
Read what is happening at each level. The tiers array is a compile-time table built from a literal type. The four static_assert lines force discountFor to run at compile time on four representative inputs, and they double as regression tests. The two constexpr finalPrice calls in main are evaluated at compile time too, so the binary already knows that 3 items at $9.99 totals $29.97 and 25 items totals $212.2875. Only the third call, finalPrice(9.99, customerQuantity), runs at runtime, and it uses the same function with the same logic.
If you change the discount rules, the tests catch it. If you add a tier, the compile-time finalPrice calls update automatically. If a customer enters an unusual quantity, the runtime path handles it. One function, three lives.
10 quizzes