-
Notifications
You must be signed in to change notification settings - Fork 396
constexpr: Your New Best Friend
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!
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.