Chapter 4.4

Con.5: Use constexpr for values that can be computed at compile time

From const to constexpr

Prior to C++11, const machinery was restricted to two things: qualifying a type as const, and thus any instance of that type as immutable, and qualifying a nonstatic member function such that *this is const in its body, like this:

class int_wrapper {
public:
  explicit int_wrapper(int i);
  void mutate(int i);
  int inspect() const;

private:
  int m_i;
};

auto const i = int_wrapper{7}; // i is of type int_wrapper const
// i.mutate(5); // Cannot invoke non-const qualified
                // member function on const object
auto j = i.inspect(); // Assign from inspect

We are sure you are familiar with this. You may also be familiar with the keyword mutable that qualifies nonstatic member data as immune from const restrictions, par-ticularly if you have read Chapter 3.4, ES.50: “Don’t cast away const.” This is part of the const machinery insofar as it applies to const-qualified member functions.

New to C++11 was the keyword constexpr. The idea behind this was to improve compile-time programming by allowing a very limited set of functions to be executed at compile time. Engineers were already doing this in an unpleasant way using pre-processor macros. With the arrival of constexpr these macros could be eliminated and replaced with proper, type-safe functions. It was a nice feeling being able to elim-inate another use case from the preprocessor.

It was not without constraints, though. You were allowed only one expression per function, which led to a resurgence in recursion and a lot of ternary operator use. The standard example, as for template metaprogramming, was generating factorials and Fibonacci numbers, but a lot of math functions were available, such as trigono-metric expansions. A single expression was very limiting but sharpened everyone’s thoughts about functional programming.

Here is a factorial example:

constexpr int factorial(int n) {
  return n <= 1 ? 1 // Base case
               : (n * factorial(n - 1)); // Recurse case
}

Of course, all this recursion highlighted to everyone what the limits of the compiler were; 12! is the largest factorial to fit in a 32-bit representation.

It was fantastically popular, and C++14 broadened the scope still further. The single-expression restriction was lifted and constexpr libraries started to appear. Particularly, decent compile-time hashing became possible. The genie was well and truly out of the bottle and nothing was going to stop the expansion of constexpr throughout the language, and the relaxing of constraints.

The constexpr facilities are becoming a language within the language, supplanting the preprocessor in many places with a type-safe, scope-aware variation.

Now you could write your factorial function using plain old if-then statements:

constexpr int factorial(int n) {
  if (n <= 1) return 1; // Base case
  return n * factorial(n - 1); // Recurse case
}

C++17 brought yet more innovations. Lambdas could be constexpr. This might seem a little peculiar, but if you think of a lambda expression as syntactic sugar for a class with a function operator, a constexpr lambda has a constexpr function opera-tor. Additionally, a new construct was introduced: if constexpr. This allowed you to eliminate some cases of std::enable_if and reduce the number of overloads for your function templates. It simplified code still further, and reduced compilation time and code comprehension burden by eliminating some of the cost of SFINAE.

C++20 introduced what would have seemed truly bizarre 10 years earlier: constexpr virtual functions, try/catch blocks in constexpr functions, constexpr STL algorithms, and constexpr dynamic allocation leading to constexpr std::string and constexpr std::vector. There is so much available to you now at compile time. You can create extensive libraries that are entirely constexpr. Hana Dusíková astonished the community at CppCon in 2017 with a compile-time regular expression library.1

1. https://www.youtube.com/watch?v=3WGsN_Hp9QY

The constexpr facilities are becoming a language within the language, supplant-ing the preprocessor in many places with a type-safe, scope-aware variation.2 This guideline was first proposed back in the C++14 days but its scope could be reasonably expanded to “use constexpr wherever possible” or even “use constexpr by default.”

2. Gabriel Dos Reis and Bjarne Stroustrup: General Constant Expressions for System Programming Lan-guages. SAC-2010. The 25th ACM Symposium on Applied Computing. March 2010.

Default C++

C++ has something of a reputation for getting its defaults wrong. For example, non-static member functions are mutable by default, when it would be safer to make them const instead. Changing state requires more thinking, reasoning, and apprehension than inspecting state, so making a nonstatic member function mutable should be an active choice: the engineer should be thinking, “I have chosen to make this a mutable function, and I’m going to signal that.” As the standard stands, it is possible for a nonstatic member function to behave as if it were const-qualified, but if the engineer fails to apply the qualification that information is not passed to the client. Not allow-ing things to be changed by default is safer than allowing them to be changed.

This can be extended to types. An instance of a type is mutable by default. Only if the type is const-qualified does an instance of it become immutable. Again, it is safer to prevent things from being changed without explicit permission than it is to allow them to be freely changed. An object can behave as if it were const without being qualified, when it would be clearer if everything were const unless otherwise specified.

The [[no_discard]] attribute is another candidate for default operation. It looks like this:

[[no_discard]] bool is_empty(std::string const& s)
{
  return s.size() == 0;
}

When return values holding error codes are discarded, that is allowing errors to be ignored. The calling code should at least acknowledge that it is discarding the error. Similarly, consider pre-increment and post-increment. The pre-increment operation will increment the object in question and return it. The post-increment operation will take a copy of the object, increment the object, and return the copy. If post-increment is used and the returned copy is discarded, then storing the copy was wasted effort. Nor can the as-if rule come to your rescue here: unless the com-piler knows that there is no observable difference between the two, for example in the case of a built-in type, it is not permitted to exchange the post-increment for a pre-increment. A [[no_discard]] attribute would signal that wasted code was being executed. Finally, empty() returns a bool and is regularly confused with clear(). A [[no_discard]] attribute would signal that the return value from a function that does nothing to the object was being ignored, rendering the call redundant.

However, in the face of this criticism, one needs to think about the history of C++. It was developed on top of C, and the decision was made to ensure that C pro-grams would still compile using a C++ compiler. If these defaults had been swapped round, all C programs would have failed. The success of C++ is due in large part to its compatibility with existing code. It is easy to introduce a new language. For some engineers it is a hobby pursuit. Getting other engineers to use it is very hard, and demanding a complete rewrite of existing code is a very high price to pay.

Using constexpr

As you can see from the examples, the constexpr keyword is simple to use. Simply decorate your function with the constexpr keyword and you are good to go. How-ever, consider this function:

int read_number_from_stdin()
{
  int val;
  cin >> val; // What could this possibly yield at compile time?
  return val;
}

Clearly, this makes no sense as a constexpr function since part of its evaluation can only take place at runtime: interaction with the user.

You might be asking yourself, “Why isn’t the entire standard library simply constexpr now?”

There are two main reasons. First, the standard library is not entirely amenable to constexpr decoration. For example, file stream access is dependent upon the client file system, and decorating std::fopen as constexpr would be meaningless in most situations, just as a function collecting input from stdin would be. Functions like std::uninitialized_copy also suffer from the same problem. What does uninitial-ized memory mean in a constexpr context?

Second, in some cases it has yet to be proposed. To put forward a paper titled “Make the standard library constexpr” would require an enormous amount of word-ing changes to the standard. C++20 devotes 1,161 of its 1,840 pages to the standard library. Such a paper would be shot down before it reached serious consideration during committee time. Additionally, just finding out which functions could be constexpr is a mammoth task. Any function called by a constexpr function must be constexpr as well, which means you would end up chasing down constexpr until you encounter a leaf function or a function that cannot be constexpr and working your way back to where you started from. Building that tree is not a pleasant task. When const first started becoming a serious programming tool, it would not be unusual to chase const through your call graph and hit functions that could not be const. Once you start down this road, you cannot stop, because const and constexpr are both viral, spreading through all they touch, which is both their beauty and their cost.

However, there are still remaining candidate containers that could indeed be constexpr. It is entirely feasible for you to write your own constexpr map and unordered_map with identical APIs to std::map and std::unordered_map. With std::vector, std::string, and a constexpr map, ordered or unordered, you can write some very useful compile-time parsers that can be used to configure your builds.

Let’s look at a simpler example. We want to calculate sin x using the Taylor series, which looks like this:

Image

The only functions we need to achieve this are a power function and a factorial func-tion. Summing the terms will complete the job.

We already have a factorial function. Let’s add a power function:

constexpr double pow(double t, unsigned int power) {
  if (power == 0) return 1.0;
  return t * pow(t, power - 1);
}

We can now add a sin function:

constexpr double sin(double theta, int term_count) {
  auto result = 0.0;
  while (term_count >= 0) {
    auto sign = pow(-1, (term_count));
    auto numerator = pow(theta, (2 * term_count) + 1);
    auto denominator = factorial((2 * term_count) + 1);
    result += (sign * numerator) / denominator;
    --term_count;
  }
  return result;
}

The term_count value is a precision request. Each successive term is smaller than the last, and eventually the contribution will be negligible. There is another reason for introducing this parameter, which we will come to in a moment. We can complete the example with a main function:

#include <numbers>

int main()
{
  return sin(std::numbers::pi / 4.0, 5) * 10000;
}

For exposition, we are invoking the function and then turning it into an integer so that it can be returned.

If you look at this on the Compiler Explorer page for this chapter, you will see two lines of assembly:

mov eax,7071
ret

sin π/4 in radians is 1/√2, which is about 0.7071, so we can be assured of the correct-ness of this function. If you are interested in the accuracy of the function, you can try increasing the scalar multiplier to 1 million, or you can try increasing the term count.

Increasing the term count is where things get interesting. At the time of writing, using gcc trunk, if you increase the term count to 6, then code is generated for the pow function. The pow function is no longer amenable to compile-time evaluation and calculation is deferred to execution time. The pow function must calculate π/4 raised to the thirteenth power, which seems to be the point at which the compiler decides that we are asking too much of it. Increase the term count to 8 and code is generated for the factorial function as well.

Another useful feature of constexpr is that it forbids undefined behavior and side effects in the function it qualifies. For example, consider this dangerous piece of code:

int get_element(const int *p) {
  return *(p + 12);
}

int main() {
  int arr[10]{};
  int x = get_element(arr);
}

The problem is quite clear in this case: the function get_element reads the twelfth ele-ment of the array while the array being passed only has 10 elements. This is unde-fined behavior. Consider what happens when you make everything constexpr (apart from main(), which cannot be constexpr):

constexpr int get_element(const int *p) {
  return *(p + 12);
}

int main() {
  constexpr int a[10]{};
  constexpr int x = get_element(a);
  return x;
}

This yields the following compiler error:

example.cpp
<source>(8):
  error C2131: expression did not evaluate to a constant
<source>(2):
  note: failure was caused by out of range index 12;
        allowed range is 0 <= index < 10
Compiler returned: 2

Forbidding undefined behavior in constexpr functions highlights places where you are taking advantage of it. You should make this choice consciously and decide whether exploiting undefined behavior is a price you want to pay. Take care when using constexpr in this way, though. The following code compiles quite happily:

constexpr int get_element(const int *p) {
  return *(p + 12);
}

int main() {
  constexpr int a[10]{};
  return get_element(a);
}

This is worth experimenting with at the Compiler Explorer page for this chapter.

Forbidding side effects in constexpr functions means that they become pure func-tions. They are easier to test and the results can be cached, but best of all they can be invoked concurrently without synchronization. This is an excellent burden to be relieved of.

This is the point at which we should talk about consteval. First, though, we are going to talk about inline.

inline

The inline keyword is an old, old keyword. It predates C. In C++ the specifier can be applied to a function or an object. It signifies that the definition can be found in the translation unit. It also tells the compiler that the function is a suitable candidate for inline substitution rather than a function call. This makes it a target for optimiza-tion: why pay the price of a function call when you can simply substitute the code instead?

There are in fact several reasons why you might not want to do that.

When a function is called, some CPU context must be preserved so that the call-ing function can continue upon return. Register values are usually put on the stack, and then restored after the function finishes executing. Choices of calling convention dictate whether this is done by the caller or the callee. The execution of these instruc-tions for context preservation incurs a cost in program size and execution speed. Ideally, an inline substitution will be cheaper than all this bookkeeping. However, if the function being substituted is particularly large, it may make the program bigger. Nonetheless, you may want the inline substitution to take place because it will yield faster code: you are exchanging a few bytes of executable size for the performance gain of not preserving registers and jumping somewhere else.

In the last century that was a quite reasonable thing to assess. However, now we have instruction caches. If we make a substitution, we are in danger of filling up the instruction cache and forcing a cache miss. If we are in a loop and the inline func-tion is only called occasionally, it may make more sense to keep it out of the loop. The compiler knows much more about the behavioral characteristics of the proces-sor than you do, so this is a decision best left to the compiler. In addition, the as-if rule allows the compiler to perform an inline substitution on any function that may improve performance. After all, the whole point of functions is that they are invisible and convenient chunks of code Somewhere Else. Their location is irrelevant. They could exist within the call site, or far away from it. This should lead to the realization that the inline keyword is largely redundant. It is a hint to the compiler that the func-tion is a candidate for substitution, but it is not a requirement, nor should it be. In fact, the only place that it might need to be used is if the function it specifies is likely to appear in more than one translation unit. This is likely to be the case if it is defined in a header file.

Some implementations offer a custom specifier with a name like __forceinline; this means “This is my decision. I am perfectly aware that I am potentially shooting myself in the foot. I believe I have additional knowledge that you, the compiler, do not.” Typically, if the function cannot be inlined, the compiler will emit an error and tell you that you are, in fact, quite mistaken.

The important thing to remember is that inline is a hint, and __forceinline, if offered by the implementation, is a command.

consteval

As you may have seen from experimenting with the constexpr sin function, you can place sufficient stress on the compiler for it to give up and defer calculation to runtime. Just as inline is a hint to the compiler to substitute the entire function body for a call to the specified function, so is constexpr a hint for the compiler to attempt compile-time computation. It is not a requirement, and if it fails it will do so silently. The keyword means only that it can be evaluated at compile time, not that it must. If you pass values that can only be known at runtime, then it will simply postpone evaluation to runtime.

There are reasons why you may not want this to happen. You may have something that can spiral out of control under certain circumstances; for example an iterative function system that never stabilizes. It would be safer to be able to rely on the com-piler to only invoke the function when it can do so safely.

This is the purpose of consteval. It specifies that a function is an immediate func-tion, which means that every call to the function must produce a value computable at compile time. This is the analog to __forceinline, in that the compiler will emit an error if the specified function cannot produce such a value.

consteval should be handled with care. It is not a guarantee to the user that the expression will be evaluated at compile time. It is a requirement placed upon the user that they must constrain their inputs such that they do not burn the compiler. This is not at all portable, nor is it necessarily obvious to the user what the actual mani-festation of these constraints is likely to be. If you experimented broadly with the sin function in Compiler Explorer, you may have seen that some implementations never compute a result at compile time. Marking those functions as consteval would lead to a world of pain for your users. You might decide to start by marking your functions as constexpr; then, should you observe run-time evaluation that you would rather happen at compile time, you can modify that to consteval.

constinit

We shall complete this chapter with the constinit keyword. To do this, we need to be clear about zero initialization and constant initialization. Initialization is, sadly, a huge topic worth a book on its own, so we shall only focus on these two aspects of C++ initialization.

Zero initialization is a fine example of good naming. It means setting the initial value of an object to zero. There is a small problem with C++ in that there is no special syntax for zero initialization. It occurs in a variety of situations, for example:

static int j;

Ordinarily, if you declare an object without initializing it, then you have no guaran-tee of its value unless there is a default constructor. Since int has no constructor, you might expect j to be random bits. However, there is a special exception for objects of static duration: they are zero-initialized as part of static initialization, before main() is called. Particularly, the value is zero explicitly converted to the type.

If the object is of a class type, then all base classes and nonstatic data members are zero-initialized. The constructors are ignored, which is worth bearing in mind. Some implementations provide default initialization of unspecified objects as a command-line switch. This is useful for debugging because the default value is normally an unusual bit pattern, so uninitialized objects are easy to spot via memory inspection.

Constant initialization sets the initial value of static variables to a compile-time constant. If the type has a constexpr constructor and all the arguments are constant expressions, then the variable is constant-initialized. If the type does not have a con-structor, or it is value-initialized, or the initializer is a constant expression, then the variable is also constant-initialized. For example:

const int j = 10;

is constant-initialized if it is declared at file scope and thus has static duration. Con-stant initialization takes place prior to static initialization. In practice, it takes place at compile time.

The constinit keyword asserts that an object has static initialization, causing compilation to fail if it does not. For example:

constinit const int j = 10;

This is a somewhat redundant use since that expression can never fail this assertion. A more interesting example would be:

// definition.h
constexpr int sample_size() {
  // function definition
}

// client.cpp
constinit const int sample_total = sample_size() * 1350;

This expression will emit an error if the author of sample_size() changes the func-tion in such a way that it can no longer be computed at compile time. This is a valu-able feature. Objects marked as constinit are evaluated at compile time, not at runtime. This gives us a mitigation for the static initialization order fiasco.

Summary

Using the const prefixed keywords gives you additional opportunities to offer com-putation at compile time. Prefer compile-time computation to run-time computa-tion: it is cheaper for the client.

  • Use constexpr to hint to the compiler that a function is amenable to compile-time computation and to detect some undefined behavior.

  • Use consteval to insist to the compiler that a function is amenable to compile-time computation on pain of error.

  • Use constinit to assert that an object is initialized at compile time.