Background
C++ is an old language.
The excitement that C++ generated in the 90’s had faded by 2010, probably because of the emergence of several newer languages like Java and also because the Standards Committee had released only a few enhancements in the decade.
And then came C++11. The specification for C++11 was approved in 2011 (hence, the name). C++11 packed a lot of features both to the core language and to the standard library (std). These features were improved in C++14 and C++17.
The features were so revolutionary that C++11, and the versions that came after it, came to be known collectively as “modern C++”.
constexpr is one such feature that was introduced in C++11 and improved in later versions.
Why should I care?
Consider a simple program to calculate the area of a rectangle.
int area(int length, int breadth) {
return length * breadth;
}
int x = area(2, 3);
This code compiles to the following assembly on GCC 10.2
area(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
x:
.zero 4
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 8
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L5
cmp DWORD PTR [rbp-8], 65535
jne .L5
mov esi, 3
mov edi, 2
call area(int, int)
mov DWORD PTR x[rip], eax
.L5:
nop
leave
ret
_GLOBAL__sub_I_area(int, int):
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
That’s pretty intimidating.
Now lets add the keyword constexpr to the function.
constexpr int area(int length, int breadth) {
return length * breadth;
}
int x = area(2, 3);
And this is what the above code compiles to:
x:
.long 6
Yes, that’s it! The compiler evaluates the function at compile-time and simply uses the result of the function in the assembly. The function is not called at run time.
What is constexpr?
The constexpr specifier is used to declare that the value of a variable or a function can be evaluated at compile time. Such variables and functions can then be used where compile-time constant expressions are allowed.
The keyword itself is a blend (or if you are linguistically inclined, a portmanteau) of constant expression, meaning that it allows you to evaluate expressions at compile time.
Note that I specifically say allows and not guarantees. A constexpr variable is guaranteed to be evaluated at compile-time. A constexpr function may be evaluated at compile-time, if it satisfies certain requirements. Otherwise, it is evaluated at runtime.
constexpr variables
constexpr double pi = 3.14;
static_assert(pi == 3.14);
The fact that the above compiles successfully means that pi is a compile-time constant.
If the specifier constexpr is removed, the compilation fails with an error.
double const pi = 3.14;
static_assert(pi == 3.14); // this fails to compile
error: non-constant condition for static assertion
2 | static_assert(pi == 3.14);
| ~~~^~~~~~~
Note that the compilation fails in the exact same way, even if pi is declared const.
All constexpr variables are const. The difference between a const and constexpr variable is that the initialization of a const variable can be deferred until run time. A constexpr variable must be immediately initialized when declared.
constexpr int unknown; // error! uninitialized
A constant expression can only be evaluated using literal types or other constant expressions (constant expressions are also literal types).
constexpr int length = 100;
int breadth = 5;
constexpr int area = length * breadth; // error! breadth is not a constant expression
Since these values are known at compile-time, constexpr values can be used anywhere compile-time constants are used. For example, we can use constexpr values to define array bounds or as non-type template argument.
#include <array>
constexpr int a = 10;
int houses[a];
std::array<int, a> cars;
static_assert(sizeof(houses)/sizeof(houses[0]) == 10);
static_assert(cars.size() == 10);
constexpr functions
A constexpr function is a function whose return value can be evaluated at compile time if the calling code requires it. Such calling code usually initializes a constexpr variable or provides a non-type template argument.
If a function that is declared constexpr cannot be evaluated at compile time, for example when its arguments are not constant expressions or no caller requires the return value at compile time, then the function is evaluated at run time like a regular function.
constexpr int fib(int n) {
if (n == 0 || n == 1) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
int one = 1;
int fib_1 = fib(one); // evaluated at run time
int fib_2 = fib(2); // maybe evaluated at run time
constexpr int fib_3 = fib(3); // guaranteed to evaluate at compile time
Note that in C++11, constexpr functions are allowed to have just a single statement and that should be a return statement. Hence, constexpr functions in C++11 extensively use ternary operators and recursion.
In C++14, a constexpr function can have body with local variables like regular functions. Thus, the above example works for C++14 only.
C++20 introduces consteval which guarantees that the function is evaluated at compile time (or the compilation fails if it cannot be evaluated at compile time).
Constructors can also be constexpr. constexpr constructor lets you define literal user-defined types.
Why constexpr ?
As we saw, the constexpr specifier can give useful hints to the compiler allowing it to optimize the run time code by doing as much work as possible during compile time. Smaller assembly code is usually faster code. Hence, the run time performance is improved.
Smaller code also means that there is a higher likelihood of the program to fit in the processor cache, which again improves performance.
Embedded software can also benefit from a smaller execution footprint because embedded systems, by their very nature, are constrained on memory and compute power.
Computations performed at compile time allow you to detect errors or bugs at compile time. This is better than encountering those errors at run time, in most of the cases.
Note that constexpr usually increases time taken to compile the program. This is because the compiler is doing more work. Or in other words, some of the run time cost is being moved to compile time.
Summary
constexpr is a specifier introduced in C++11 and improved in C++14. It allows you to evaluate expressions at compile time.
The major difference between constexpr variables and constexpr functions is that:
- constexpr variables necessarily need to be constructed and destructed at compile time
- constexpr functions permit compile time evaluation if whatever they do is allowed at compile time - they do not necessarily always run at compile time
Using constexpr appropriately in your code can improve runtime performance and produce smaller executables, but at the cost of increased compilation times.
