MODERN C++ IN EMBEDDED DEVELOPMENT: constexpr

What is constexpr? 

Cppreference defines constexpr specifier as follows: “The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time. Such variables and functions can then be used where only compile-time constant expressions are allowed (provided that appropriate function arguments are given).”

constexpr variables

A constexpr variable must be immediately initialized, and its type must be a LiteralType. Other requirements must be met, but these two are most important for starting utilizing the benefits constexpr brings.  

The basic usage of constexpr variables is compile-time constants. Here is a simple example:

constexpr double pi = 3.14159265359;

If it’s not used in runtime (passing a pointer or a reference of the pi to a function), the compiler will optimize pi away. That is, it will not exist in memory as a variable. 

constexpr functions

If we add a constexpr specifier to a function, it is a hint to the compiler that the function can be executed in a compile-time if provided with constant arguments. Some of the requirements for the constepxr function are: the return type must be a LiteralType, each of its parameters must be a LiteralType, and if the function is not a constructor, it needs to have precisely one return statement. Another important requirement imposed on the constexpr function is that they cannot have variables that are not initialized. Let’s take a look at a simple example of a square constexpr function:

constexpr int square(int a) {
    return a*a;
}

int main() {
    constexpr int ret = square(2);
    return ret;
}

If the square function is executed in compile time, we should see that by examining the assembly code of the above example. Using compiler explorer, we can see that the assembly output for the above example is the following:

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 4
        mov     eax, 4
        pop     rbp
        ret

You don’t need to know assembly for x86-64 architecture to realize that there is no square function in generated assembly code. Let’s remove the constexpr specifier for the above example and compare the assembly generated by the compiler. 

int square(int a) {
    return a*a;
}

int main() {
    int ret = square(2);
    return ret;
}

Below is the assembly code generated by the compiler:

square(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        imul    eax, eax
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     edi, 2
        call    square(int)
        mov     DWORD PTR [rbp-4], eax
        mov     eax, DWORD PTR [rbp-4]
        leave
        ret

We can see that without a constexpr specifier actual square function is generated and called. Note that the optimization level in both examples is -O0. The compiler used for the above examples was GCC 11.2 for x86-64. 

Macros vs. constexpr 

Define macros are a classic approach to defining constants in C. It’s also common in some C++ codebases. By introducing the constexpr specifier in C++11, C++ provides a better way. Let’s explore this example:

#include <cstdio>

#define VOLTAGE 3300
#define CURRENT 1000

int main() {
    const float resistance = VOLTAGE / CURRENT;
    printf("resistance = %.2f\r\n", resistance);
    return 0;
}

What’s the output of the example above? It’s:

resistance = 3.00

So, what happened? Both voltage and current are parsed as integer literals, and so is the result of expression voltage / current. If we’d make either voltage or current a floating-point literal using the f suffix

#define VOLTAGE 3300.f
#define CURRENT 1000.f

We’d get the expected result:

resistance = 3.30

You could argue that this is basic knowledge, but imagine a situation where you are trying to tweak a parameter, changing values of voltage and current constants, and you forget the f suffix. We can do better. Using constexpr specifier, we can have type-safe constants:

#include <cstdio>

constexpr float voltage = 3300;
constexpr float current = 1000;

int main() {
    const float resistance = voltage / current;
    printf("resistance = %.2f\r\n", resistance);
    return 0;
}

Now the compiler is aware of the type of our constants, and there is no more need for the f suffix. The output is as expected:

resistance = 3.30

Using the constexpr specifier compiler gets insight into your variable type-ness intentions. Also, you and anyone else reading the codebase knows the type intended for the constants used. 

Initialize array using constexpr functions

Generation of signals using PWM or DAC modules is a common task in embedded development. The old-school approach is to use header files with signal arrays. These header files are generated by third-party software or scripts. Using constexpr, we can create signal arrays in compile time.

Let’s take, for example, a trivial signal of square function. We want to be able to set the number of elements in signal and delta time. For the sake of simplicity, we will use int as type. The following example demonstrates the usage of constexpr for generation of square signal in compile time:

#include <array>
#include <iostream>

template <std::size_t N, int delta>
constexpr std::array<int, N> generate_array() {
    std::array<int, N> arr = {0};
    for (int i = 0; i < N; i++) {
        arr.at(i) = (delta * i) * (delta * i);
    }
    return arr;
}

int main() {
    auto signal = generate_array<5, 2>();

    for (auto const& elem : signal) {
        std::cout << elem << std::endl;
    };

    return 0;
}

The output of the above example is:

0
4
16
36
64

In case we need to initialize constexpr array only once and that generate function is not needed we can utilize constexpr lambda (available from C++17) as follows:

#include <array>
#include <iostream>

constexpr std::array<int, 3> signal = []() constexpr {
    std::array<int, 3> arr = {0};
    for (int i = 0; i < 3; i++) {
        arr.at(i) = (2 * i) * (2 * i);
    }
    return arr;
}();

int main() {

    for (auto const& elem : signal) {
        std::cout << elem << std::endl;
    };

    return 0;
}

The output of the above example is:

0
4
16

The above examples show the power of constexpr specifier and use cases in embedded system development. The generation of custom signals in the codebase is a considerable advantage compared to generating these signals in custom scripts. It also helps with testing the business logic modules as you can write the tests in the same language and without calls to external scripts when you want to see how your module behaves with different signals.

Amar Mahmutbegović

Amar Mahmutbegović

Head of Engineering

Embedded developer with a proven history in the development of BLE embedded firmware in C and C++. Co-founder of Semblie.

Semblie is a hardware and software development company based in Europe. We believe that great products emerge from ideas that solve real-world problems.