Constexpr in C++ (ENG)

Make Compile-Time Great

We always want our programs to be faster and faster, and people are constantly searching for algorithms that make our programs run swiftly at runtime. Typically, these algorithms calculate a number or perform other tasks. The lower the time complexity, the better the algorithm.

Since C++11 introduced the constexpr specifier, we can evaluate the value of entities at compile time, allowing the compiler to determine the results during the compilation process. This can significantly improve runtime performance by shifting some of the work from runtime to compile-time, thus it's called compile-time optimization.

Additionally, with the constexpr specifier, you can catch errors at compile time, making the code even safer. However, there is a trade-off because performing all calculations at compile-time makes the process compiler-dependent. The result may vary with different compilers.

Examples First

Let's first go dive in a real example to see how the compile-time optimization happens. This code example following below will serve as our first example today:

#include <iostream>

int add(int a, int b){ // No constant expression
    return a + b;
}

int main() {
    int a = 100;
    int b = 200;    
    int c = add(a, b);
    return 0;
}

This example demonstrates a simple function add that adds two integers. In the main function, we call add with a and b as arguments. Currently, the function does not use constexpr, so the addition is performed at runtime.

After compilation:

add(int, int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     edx, DWORD PTR [rbp-4]
        mov     eax, DWORD PTR [rbp-8]
        add     eax, edx
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 100
        mov     DWORD PTR [rbp-8], 200
        mov     edx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, edx
        mov     edi, eax
        call    add(int, int)
        mov     DWORD PTR [rbp-12], eax
        mov     eax, 0
        leave
        ret

So you can see how many assembly instructions there are needed to be execute in the run-time.

With the using of constexpr , our C++ code turns into:

#include <iostream>

constexpr int add(int a, int b){
    return a + b;
}

int main() {
    constexpr int a = 100;
    constexpr int b = 200;    
    constexpr int c = add(a, b);
    return 0;
}

After compilation without any compiler optimization:

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 100
        mov     DWORD PTR [rbp-8], 200
        mov     DWORD PTR [rbp-12], 300
        mov     eax, 0
        pop     rbp
        ret

You see, we don't have the add() function symbol here. Why? You may have guessed it, the constexpr specifier implies inline semantics.

Fibonacci with constexpr

The previous example might not be very intuitive, now let's do another experiment to see how awesome constexpr can be.

#include <iostream>

int fibonacci(int n) {
    return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}
int main() {
    int fib10 = fibonacci(10);
    int fib20 = fibonacci(20);
	int fib30 = fibonacci(30);
	int fib40 = fibonacci(40);
    std::cout << "Fibonacci(10): " << fib10 << std::endl;
    std::cout << "Fibonacci(20): " << fib20 << std::endl;
    std::cout << "Fibonacci(30): " << fib30 << std::endl;
	std::cout << "Fibonacci(40): " << fib40 << std::endl;
    return 0;
}

This code calculates the Fibonacci sequence at runtime. The output might look like this:

du@DVM:~/cpp$ time ./proc 
Fibonacci(10): 55
Fibonacci(20): 6765
Fibonacci(30): 832040
Fibonacci(40): 102334155

real    0m0.604s
user    0m0.600s
sys     0m0.003s

And with constexpr, our source code look like this:

#include <iostream>

// constexpr function to calculate Fibonacci numbers at compile-time
constexpr int fibonacci(int n) {
    return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}

int main() {
    constexpr int fib10 = fibonacci(10);
    constexpr int fib20 = fibonacci(20);
	constexpr int fib30 = fibonacci(30);
    std::cout << "Fibonacci(10): " << fib10 << std::endl;
    std::cout << "Fibonacci(20): " << fib20 << std::endl;
    std::cout << "Fibonacci(30): " << fib20 << std::endl;
    return 0;
}

With the use of constexpr, the output might look like this:

du@DVM:~/cpp$ time ./proc 
Fibonacci(10): 55
Fibonacci(20): 6765
Fibonacci(30): 832040
Fibonacci(40): 102334155

real    0m0.005s
user    0m0.003s
sys     0m0.002s

You can see how crazy the difference is—in user time, the optimized version is 200 times faster! This demonstrates the significant impact of compile-time optimization using constexpr.

constexpr Explained

We have demonstrated the main idea about the constexpr specifier, which is to evaluate the value of the expressions at compile time. This means the compiler will provide a constant value at the compile-time (or literals, if you will). For this reason, the object declared by constexpr would imply a const semantics.

For example:

constexpr int i = 100;
i = 200; // error

int j = 100; // j is a regular value, which can be modified at runtime. A constant expression must not depend on runtime values.
/*
there might be some j value modification operations...
so you cannot say the j value down below in the compile-time
*/
constexpr int k = j; // error

Here, we have some points to keep in mind. You must noted the right-hand side of constexpr variable declaration must be a constant expression. Because everything should be figured out at compile-time, thus everything should be known at compile-time. Otherwise, an error will occur.

Also, we saw the function declared by constexpr implies an inline semantics, and since C++17, the static data member declared by constexpr would imply inline semantics as well.

#include <iostream>
class myClass {
public:
	constexpr static int a = 50;
	// Equivalent to inline const static int a = 50;
	constexpr int a_square();
};
constexpr int myClass::a_square(){ // myClass::a_square would be inlined
	return myClass::a * myClass::a;
}

int main() {

    myClass obj;
    constexpr int i = obj.a_square();
    return 0;
}

And variable/function template can be declared constexpr too.