Get to know constexpr

I did my first run with C++ before modern C++ was a thing. That is, before C++11 and C++14. Now that I’m once again developing application code using C++ I’ve learned to master great things like smart pointers, move semantics, and lambda expressions. One modern C++ concept that I haven’t put so much thought to is the keyword constexpr. But now it’s time, let’s get to know constexpr.

First, let’s break the word itself. It is short for constant expression and was introduced in C++11 and improved in C++14. Constant means something that does not change, and expression is a sequence of operands and operators. A really simple example is assigning a value to a variable that is not allowed to be changed:

constexpr auto n = 42;

This sets n to the value 42 and does not allow n to be changed to anything else. This allows the compiler to optimize away n and insert the value 42 in all places where n is used. We can take this a step further and use n to calculate other constexpr values:

constexpr auto half_n = n / 2;

Now, both n and half_n are calculated at compile time and no CPU cycles needs to be used during runtime to perform this calculation.

You can also use constexpr with functions, i.e. you can mark a function with constexpr, like this:

constexpr int pow(int base, unsigned exp) {
  if (exp == 0) return 1;
  return base * pow(base, exp-1); // Note the recursive call
}

With this function you can calculate things like 216 during compile time, as long as the arguments given to pow are also constexpr (or literals). But what if you want to use pow with values that are not known at compile time, say for example with values requested from the user or read from a data source? Great news! It is perfectly fine to use pow just like a regular function, if the argument values are not constexpr or literals then pow will be called at runtime instead:

constexpr int pow(int base, unsigned exp) {
  if (exp == 0) return 1;
  return base * pow(base, exp-1);
}

int main() {
  constexpr auto bits_per_byte = pow(2, 3); // Calculated at compile time
  int n;
  std::cin >> n;
  const auto user_value = pow(2, n); // Calculated at runtime
}

Since bits_per_byte is calculated at compile time it can be used in constructs that requires constant values, for example when defining a std::array: std::array<bool, bits_per_byte> byteArray{};

So far we have only used simple built-in types with constexpr, but it can also be used with user-defined types. For example we can define a Point3D type that holds three integer values representing coordinates on the x-, y-, and z-axis:

class Point3D {
public:
  constexpr Point3D(int x = 0, int y = 0, int z = 0) noexcept
    : x{x}, y{y}, z{z} {}

  constexpr int getX() const noexcept { return x; }
  constexpr int getY() const noexcept { return y; }
  constexpr int getZ() const noexcept { return z; }

  constexpr void setX(int new_x) noexcept { x = new_x; }
  constexpr void setY(int new_y) noexcept { y = new_y; }
  constexpr void setZ(int new_z) noexcept { z = new_z; }
private:
  int x, y, z;
};

constexpr Point3D reflectionXY(const Point3D& p) {
  Point3D res;
  res.setX(p.getX());
  res.setY(p.getY());
  res.setZ(-p.getZ());
  return res;
}

int main() {
  constexpr Point3D p{-1, 2, -3};
  constexpr auto reflected = reflectionXY(p);
}

Notice that even the setters can be constexpr, they are used in reflectionXY. As long as the values used for x, y, and z are constants that are known during compile time the Point3D class can be used by the compiler during compile time and you don’t get any runtime overhead for creating and freeing up objects. As with functions it works just as well with values not known during compile time as well.

That concludes everything I wanted to say about constexpr. Now go write some code!

Lämna en kommentar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *