C++ constexpr Functions: A Guide to Compile-Time Computation
This article will cover the details about constexpr functions .
Before start lets cover some basics first (From my previous post about constexpr variables):
C++11 introduced
constexprkeyword to allow ordinary functions and objects to participate in compile-time evaluation, making it possible to produce real compile-time constants and to write more expressive and efficient template meta-programs.
By applying constexpr to variable we can obtain a true compile time constant or lets call it as a constant expression variable.
The important Rule about constant expression variable initialization:
Aconstexprobject must be initialized with a constant expression, meaning its value must be fully known and computable at compile time. Runtime variables—even if they currently hold constant values—cannot be used, because their values are not guaranteed to be available during compilation.
Lets look at a scenario where we will try to initialize a constexpr
variable from the result of a function call, in practical there are cases you need something like this right !!
Lets write a simple calculateArea function that calculates the area of a rectangle and the returned area is stored in a constexpr.
#include <iostream>
int calculateArea(int l, int w) {
return l * w;
}
int main() {
constexpr int length{10};
constexpr int width{10};
constexpr int area{calculateArea(length, width)};
return 0;
}The above code fail to compile with error since constant expression initialization expects a constant expression but calculateArea doesn't produce/returns a value that is a constant expression:
constexpr.cpp:10:19: error: constexpr variable 'area' must be initialized by a constant expression
10 | constexpr int area{calculateArea(length, width)};
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
constexpr.cpp:10:24: note: non-constexpr function 'calculateArea' cannot be used in a constant expression
10 | constexpr int area{calculateArea(length, width)};
| ^
constexpr.cpp:3:5: note: declared here
3 | int calculateArea(int l, int w) {
| ^
1 error generated.C++11 constexpr functions
C++11 introduced constexpr functions which are functions capable of being evaluated at compile time and produce a constant expression as its return value.
Syntax:
The syntax is simple, just write constexpr before the return type of a function signature.
constexpr return-type func_name(paramter) {
}Now lets rewrite the example using constexpr function :
#include <iostream>
constexpr int calculateArea(int l, int w) {
return l * w;
}
int main() {
constexpr int length{10};
constexpr int width{10};
constexpr int area{calculateArea(length, width)};
return 0;
}This code compiles and the function calculateArea is evaluated at compile time. Here look at the assembly, the function call is completly replaced with the result of the evaluation:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 10
mov DWORD PTR [rbp-8], 10
mov DWORD PTR [rbp-12], 100
mov eax, 0
pop rbp
retTo evaluate the function at compile-time, below things must be true:
- The call to the constexpr function must have arguments that are known at compile time (e.g. are constant expressions).
- All statements and expressions within the constexpr function must be evaluatable at compile-time or constexpr.
Note:
Just marking a a function as constexpr doesn’t guaranty that the function will be evaluated at compile time. It can be not must be. It depends on on how it is used.
To demonstrate that constexpr function can be evaluated at runtime/compile-time depending upon the context let me modify the example a bit like this:
#include <iostream>
constexpr int calculateArea(int l, int w) {
return l * w;
}
int main() {
constexpr int length{10};
constexpr int width{10};
// Will be evaluated at compile time
constexpr int compiletime_area{calculateArea(length, width)}; // Call-1
int normal_area{calculateArea(20,30)}; // Call-2
// Use user input length and width to calculate the area
int user_input_len;
std::cin >> user_input_len;
int user_input_wid;
std::cin >> user_input_wid;
// Will be evaluated at run-time
int runtime_area{calculateArea(user_input_len, user_input_wid)}; // Call-3
return 0;
}Look at the assembly:
calculateArea(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 32
mov DWORD PTR [rbp-4], 10
mov DWORD PTR [rbp-8], 10
mov DWORD PTR [rbp-12], 100 <-- Call-1 is replaced with evaluation result
mov DWORD PTR [rbp-16], 600 <-- Call-2 is replaced with evaluation result
lea rax, [rbp-24]
mov rsi, rax
mov edi, OFFSET FLAT:std::cin
call std::istream::operator>>(int&)
lea rax, [rbp-28]
mov rsi, rax
mov edi, OFFSET FLAT:std::cin
call std::istream::operator>>(int&)
mov edx, DWORD PTR [rbp-28]
mov eax, DWORD PTR [rbp-24]
mov esi, edx
mov edi, eax
call calculateArea(int, int) <-- This shows will be called at run-time
mov DWORD PTR [rbp-20], eax
mov eax, 0
leave
retSo let me rephrase our understanding, a constexpr function will be evaluated at:
- Compilation time:
- If all the parameter passed to the function areconstexprand the function body have snippet that are constant expression or results in a constexpr (this means that every statement actually executed during this call must be legal in constant evaluation) and result returned from the function must be a constant expression.
- The result is used in a context that requires a constant expression (e.g., template arguments, array sizes, or initializing aconstexprvariable).
Note: Basically the parameters, evaluation result to return must be of literal type. (Check this post for to know details about Literal Types) - Run-time: If any of the parameter passed to the function are not
constexpror the body has code snippet that can not be evaluated at compile-time(< C++20)(C++20 allows dynamic memory allocation, vector, string inside a constexpr function) or has a return type that is not a literal type(not a constant expression).
Note: A big Advantage
This capability of evaluation ofconstexpr functionin both compile-time and run-time based on the context its being used is a great advantage.
So programmers does not have to write two separate overloaded function with different name but completely duplicate code.
constexpr functions are inline by default:
Why ?
As you must be knowing to evaluate something during compilation the compiler needs to see the full definition of the constexpr function in each translation unit(like for templates). So to not violate the ODR (One Definition Rule) by default constexpr function are inline .
By making
constexprimplicitlyinline, the standard allows: The same function definition to appear in multiple translation units, but represent one function.
It makes header-onlyconstexprcode possible and idealy you should writeconstexprfunctions in headers.
constexpr is not about “compile time”.
It is about making functions usable as values.
Get Sagar’s stories in your inbox
Join Medium for free to get updates from this writer.
To do that, the function must:
- Be visible everywhere
- Be definable everywhere
- Behave as one entity
That is exactly what inline gives.
C++11 constexpr function Limitations:
constexpr function introduced in C++11 were severely limited with the following restriction:
- Must contain only a single return statement (Major Restriction)
- No local variables allowed
- No loops (for, while)
- No if statements (only ternary operator
?:) - Function body must be a single expression
// This does NOT work in C++11
constexpr int fibonacci(int n) {
if (n <= 1) return n; // Error: multiple return statements
return fibonacci(n-1) + fibonacci(n-2);
}
// This does NOT work in C++11
constexpr int sum_to_n(int n) {
int sum = 0; // Error: local variable
for (int i = 1; i <= n; ++i) { // Error: loop
sum += i;
}
return sum;
}To work around the single return statement limitation, you had to use recursion and ternary operators. For our fibonacci example code the following workaround will work in C++11
#include <iostream>
// C++11 compliant - using ternary operator
constexpr int fibonacci(int n) {
return (n <= 1) ? n : (fibonacci(n-1) + fibonacci(n-2));
}
// C++11 compliant - using recursion for sum
constexpr int sum_to_n_helper(int n, int sum) {
return (n == 0) ? sum : sum_to_n_helper(n - 1, sum + n);
}
constexpr int sum_to_n(int n) {
return sum_to_n_helper(n, 0);
}
int main() {
constexpr int fib10 = fibonacci(10);
constexpr int sum = sum_to_n(100);
std::cout << "Fibonacci(10) = " << fib10 << std::endl;
std::cout << "Sum(1..100) = " << sum << std::endl;
return 0;
}Output:
Fibonacci(10) = 55
Sum(1..100) = 5050Not pretty right !! This is awkward and hard to read. Simple iterative algorithms require complex recursive solutions.
Later standards relaxed these restrictions significantly, but covering that evolution would take us beyond the scope of this post. I will stop at the C++11 rules here, the later evolution of constexpr is a topic worthy of its own post.
Thanks for reading !! Hope you found this walkthrough both useful and insightful !!!
Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.
If you’re serious about passing technical interviews, try PracHub. It helped me structure my preparation with advanced mock sessions. [Check it out here] and start practicing. (Disclosure: This is an affiliate link).










