Skip to content

constexpr: Your New Best Friend

amirroth edited this page Feb 1, 2022 · 3 revisions

You may be familiar with the const modifier, which tells the compiler that a variable is a run-time constant, i.e., the variable's value does not change within a given scope. This allows the compiler to perform some optimizations it otherwise may not be able to perform (e.g., allocating the variable to a register) but moreso, it allows the compiler check that you are not accidentally modifying this variable and printing warnings or errors if you are.

Starting with C++11, there is a more powerful modifier called constexpr that tells the compiler that the variable in question is a compile-time constant. The const modifier tells the compiler that a variable has a constant value within the scope, but not what that value is. The constexpr modifier does tell the compiler what the value is and this allows the compiler to do all sorts of things.

The first thing it allows the compiler to do is hard-code the value into instructions themselves--many instruction sets including x86_64 have "immediate" integer instructions in which one of the inputs is a literal that is hardwired into the instruction rather than a register. Using immediate/literal instructions saves registers which are a scarce commodity in x86_64. It also eliminates the need to initialize the variable at runtime. Even if the compiler cannot hard-code the value into a register (e.g., because the value is a floating-point number and there are no immediate variants of floating-point instructions, the compiler can always load the literal value into a register without using a temporary register to hold the address. The compiler does this by storing the value in a special subsection of the executable, remembering the address where it stored it, and hardcoding the address into the "load" instruction. Again, this saves registers.

constexpr Real64 PI = 3.1415926535; // PI does not take any runtime initialization or storage.

It gets better. The compiler can evaluate expressions at compile time if the inputs of the expression are themselves constexpr. This allows the compiler to hard-code the result of the expression (or load it from a constant address in the executable) wherever the expression is used.

constexpr Real64 TwoPI = 2.0 * PI; // Neither does TwoPI

And even better. Any object or container that is not dynamically allocated can also be declared constexpr. One of the most powerful examples is the constexpr std::string_view (here is a little tutorial about std::string_view).

constexpr std::string_view programName = "EnergyPlus"; // No runtime initialization here

Here, not only is the string "EnergyPlus" a compile time constant that is stored in the executable rather than on the heap (courtesy of using std::string_view rather than std::string), but the programName symbol container itself is a compile-time constant whose members (the address of the "EnergyPlus" string in the executable and its length) are compile-time constants that the compiler can hard-code.

Here is another fun example:

constexpr std::array <std::string_view, 7> dowNames = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; // No runtime initialization here either

Remember, std::array does not heap allocate storage for its data elements, so this entire data structure is a compile-time constant as is any constant reference to it like:

constexpr monName = dowNames[1];

Finally, starting with C++14, the constexpr modifier can be used instead of the inline modifier. This tells the compiler to (partially) evaluate the function if any of the arguments are constexpr. Fun times!

Where should you define constexpr arrays and strings?

Should constexpr "variables" be defined in .hh files or should they be declared extern in .hh files and then defined in .cc files? Well, it depends on what kind of variable they are!

Scalar constexpr variables (e.g., int and Real64 variables) can be inlined into machine instructions by the compiler and so you want their actual values to be available in all compilation units, which means they should be fully defined in .hh files.

Non-scalar constexpr variables (e.g., std::array, std::string_view, and struct variables) cannot be inlined into machine instructions and have to be stored in the .text segment. Defining these in .hh files can create multiple copies of them, and can also slow down compilation by making commonly included .hh files larger than they otherwise would be. These variables should be declared extern const in .hh files and then defined in .cc files.

Clone this wiki locally