Modern C++ Features – constexpr

In the last two weeks I have written about the basics of compile time constants and calculation with those constants. This week I conclude this mini series with the keyword `constexpr` added in C++11/14.

Limits of C++03 compile time calculations

The calculations I described in the last post were either rather simple or involved template meta functions. Non-template calculations are essentially limited to one-liners. In addition, we can not reuse them but have to copy-paste them everywhere we have to do the same or similar calculations.

Template meta functions on the other hand can be extremely complicated to read. With C++14 we got variable templates which can improve the readability of template meta functions a bit. Nevertheless there is a remaining issue.

All those compile time calculations can be used solely at compile time. If we want to do the same calculation at run time, we have to duplicate the code. Since we can’t use templates or don’t want to copy-paste those one-liners around, the run time calculations will look different to the compile time calculations, which makes it hard to spot differences.

constexpr to the rescue

So what if there were functions that can be executed at compile time and run time, depending on the context? That kind of function was introduced in C++11. They are simply functions marked with the keyword `constexpr`.

With C++11 `constexpr`, the template meta programming Fibonacci function shown in the last post would be implemented like this:

constexpr unsigned fibonacci(unsigned i) {
  return (i <= 1u) ? i : (fibonacci(i-1) + fibonacci(i-2));
}

We can now use this function as well in a compile time context as during run time. Naturally, we can only use it at compile time if the provided arguments themselves are compile time constants. The compiler still has no crystal ball to know which values a run time argument might have.

int main(int argc, char** argv) {
  char int_values[fibonacci(6)] = {};       //OK, 6 is a compile time constant
  std::cout << sizeof(int_values) << '\n';  //8
    
  std::cout << fibonacci(argc) << '\n';     //OK, run time calculation
  std::cout << sizeof(std::array<char, fibonacci(argc)>) << '\n'; //ERROR
}

The last line is an error because, since `argc` is not a compile time constant, neither is `fibonacci(argc)`.

constexpr variables and literal types

Variables that are declared `constexpr` are, as the keyword suggests, constant expressions and can be used for compile time calculations. Unlike in C++03, where only literals of built in type could be compile time constants, the restriction has been relaxed in C++11 and C++14.

The category of types that can be used for `constexpr` variables is called literal type. Most notably, literal types include classes that have `constexpr` constructors, so that values of the type can be initialized calling `constexpr` functions.

Consider for example this point class which is a literal type:

class Point {
  int x;
  int y;
public:
  constexpr Point(int ix, int iy) : x{ix}, y{iy} {}
  constexpr int getX() const { return x; }
  constexpr int getY() const { return y; }
};

We can create `constexpr` variables from it, and since it has constexpr getters as well, use the values of those variables in compile time contexts:

constexpr Point p{22, 11};
constexpr int py = p.getY();
double darr[py] {};

constexpr functions

In C++11 there were pretty tight restrictions for the content of`constexpr` functions. Basically the function body was restricted to a single return statement, apart from optional `typedef`s, `static_assert`s etc.

In C++14 most of those restrictions are lifted. The most notable remaining restrictions are that there may be no try blocks and no variables of static or thread local storage. So, in C++14 the `fibonacci` function can be written in a more readable form:

constexpr unsigned fibonacci(unsigned i) {
  switch (i) {
    case 0: return 0;
    case 1: return 1;
    default: return fibonacci(i-1) + fibonacci(i-2);
  }
}

Runtime functionality in `constexpr` functions

If we use a `constexpr` function at compile time, we are not only bound to arguments that are known at compile time. The function may also only call other `constexpr` functions and it is forbidden to use any stuff that needs a run time context, such as throwing exceptions, calling `new` or `delete` and similar things.

However, that does not mean we are not allowed to write these things in a `constexpr` function. We can do this, but we may not call it for compile time calculations in a way that would try to execute those lines.

The standard actually demands, that if the evaluation of a `constexpr` function call makes the evaluation of “run time constructs” (this is not official standard wording) necessary, that function call is not a constant expression any more.

The list of those run time constructs is rather long, it includes for example calls to non-`constexpr` functions, `new`, `delete`, `throw`, `reinterpret_cast`, and “expressions that would exceed implementation defined limits”. The latter basically means that we can not run programs of arbitrary length and complexity at compile time.

The key thing is however that a `constexpr` function call remains a constant expression if no run time construct needs to be evaluated. Let’s for example build a little check against integer overflow into our `fibonacci` function:

constexpr unsigned fibonacci(unsigned i) {
  switch (i) {
    case 0: return 0;
    case 1: return 1;
    default: {
      auto f1 = fibonacci(i-1);
      auto f2 = fibonacci(i-2);
      if (f1 > std::numeric_limits<unsigned>::max() - f2) {
        throw std::invalid_argument{"Argument would cause overflow"};
      }
      return f1+f2;
    }
  }
}

This check will always work, but in different ways. If we call the function with too large a value in a run time context, we will get the `std::invalid_argument` thrown at run time. If we call it in a compile time context with such a large argument, the compiler will simply tell us that the function call is not a constant expression.

Conclusion

Compile time calculations have become a nicely usable feature in C++14. While they do increase the time it takes to compile our code, they can reduce the execution time and memory footprint of our programs. Therefore look out for opportunities to use `constexpr` and measure if it can improve your run time statistics.

Some people even recommend to at least try to make every function a `constexpr` and let the compiler decide whether it can and will execute them at compile time or not. This may however be not feasible, since it would litter our code with those extra keywords while the benefits may be questionable.

Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

7 Comments


  1. Jo

    Hello Arne,
    What you point in the conclusion is a question that I ask myself since I discovered this feature and I can’t find a correct explanation : why this keyword was added if the compiler could try to automatically add it to every functions ? Is that only a matter of compil time that would be too long ?

    Thanks !

    Reply
    1. Arne Mertz

      Hi Jo, good point. Compile times may be a reason, but I think what is more important is that compile time calculations in theory may have different results than run time calculations. This sounds scary, so let me explain: Compile time calculations are basically executed in a kind of C++ interpreter on the platform (OS, processor architecture, 32/64 bit binary etc.) the compiler runs on. That platform may differ from the platform the program is compiled for. The standard explicitly notes that e.g. rounding in floating point calculations may differ between compile time and run time. Therefore switching to implicit default constexpr would harm backward compatibility, which is a thning the standard committee strives to avoid.
      There are other issues as well. Getting the compile time interpreter for constexpr right is hard, because it is different to compilation. I managed to core dump both GCC and Clang when I tested the checked example for the fibonacci function: GCC doesn’t like `numeric_limits::max()`, while Clang probably had an internal stack overflow when I tried larger numbers. With an imperfect interpreter, trying to switch to implicit constexpr would break compilations that otherwise would have no problem, even without different results in the calculations.

      Reply

  2. The old metaprogramming version of fibonacci will automatically cache the function results, because each class will only be instantiated once from the template. Does the constexpr function do that as well?

    Reply
    1. Arne Mertz

      Hi Tama, thanks for your question. I don’t have a standard document at hand right now, but the compiler should be allowed to cache results. I’m pretty sure that it is not required to do so, though.

      Reply

  3. Small typo: at the beginning of “constexpr to the rescue
    “, you wrote “template meta programming factorial” but the example is fibonacci.

    Reply
    1. Arne Mertz

      Thanks for pointing that out, fixed it 🙂

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *