Loading...

Demystifying constexpr

7 years ago

C++ 11 and C++ 14 came with a lot of new features. People tend to focus on lambdas and rvalue references, but today I’d like to talk about constexpr.

In this post we’ll only talk about constant expressions as in C++ 14. There is absolutely no point to restrict yourself to C++ 11 in 2016. C++ 14 is really C++ 11 Service Pack 1, so if you missed the update, go straight to C++ 14!

I’ll also assume you’re sold on the benefits of compile-time computations. If not, two obnoxious guys wrote a book about metaprogramming that should convince you. Go read it, I’ll wait.

Compile time values

A compile time value, is a value known at compile time.

// val is known at compile time
int val = 3;

You may want the value to be strictly compile time. In C++ 03, you would typically write:

// val is set at compile time
// and will accept no further assignments
static const int val = 3;

Strict-compile time values enable the compiler to optimize better. You could use a macro, but macros aren’t parsed and are harder to debug. Also, every time you write a macro, your eliteness decreases.

Basic arithmetic is available for compile time values:

static const int a = 1;
static const int b = 2;
static const int a_plus_b = a + b;

Although compilers got very good at guessing what is known at compile time (even without all these fancy keywords), you should give them as much as information so that they can do the job best.

In C++ 14, the new constexpr keyword (for constant expression) has been introduced. When you are trying to produce compile time values, you should use it instead of const (note that const can still be used just the same):

static constexpr int a = 2;

The following is also valid, but doesn’t have exactly the same meaning:

constexpr int a = 2;

Static specifies the lifetime of the variable. A static constexpr variable has to be set at compilation, because its lifetime is the the whole program.

Without the static keyword, the compiler isn’t bound to set the value at compilation, and could decide to set it later.

So, what does constexpr mean?

constexpr: compile time if you can

A constant expression doesn’t mean “compile time expression” or “constant value” or “constant function”.

A non-formal description of a constant expression could be:

Given compile time input, the constant expression can compute the result.

For a variable, it’s pretty straightforward. You set the value at compile time, you have the result at compile time.

In C++ 14 (and also C++ 11 actually, just with more restrictions), the constexpr can also be applied to a function or method. What does that mean?

A constexpr function is able to compute its result at compilation time, if its input is known at compilation time.

In other words, any function that has “everything it needs” to compute its result at compile-time, can be a constant expression.

constexpr functions by example

Let’s have a look at this function:

int add(int a, int b)
{
return a + b;
}

Could we use add the constexpr keyword to this function? The answer is yes, because if a and b are known at compile time, we have everything we need to compute the result.

In other words, the following is legal:

constexpr int add(int a, int b)
{
return a + b;
}

static constexpr int val = add(3, 4);

Although the function is constexpr, you can use it with runtime value just the same:

int main(int argc, char argv)
{
return add(argc, argc);
}

And now for something completely different. Can we use the constexpr qualifier on this function?

int add_vectors_size(const std::vector<int> & a, const std::vector<int> & b)
{
return a.size() + b.size()
}

To answer this funtion, let’s go back to our rule.

Given compile time input, the constant expression can compute the result.

The compile time input are two vectors. Can we know at compile time the size of a vector? The answer is no. Thus, add_vectors_size cannot be a constant expression. We don’t have “everything we need”.

Let’s change a bit the function:

template <std::size_t N1, std::size_t N2>
int add_arrays_size(const std::array<int, N1> & a,
const std::array<int, N2> & b)
{
return a.size() + b.size();
}

It is possible to know the size of an array at compile time (the size method is constexpr, that’s a give away), therefore add_arrays_size can be a constant expression.

Limits of constant expressions

Although greatly relaxed since C++ 14, the rules of constant expressions are stricter than simply “compile time input yields compile time output”.

For example the goto statement or asm blocks are disallowed. You can read a thorough description of the constraints on C++ reference.

More constexpr, less templates

The most powerful thing about constant expressions, is that they enable you to do meta-programming without resorting to templates (don’t say farewell to TMP too quickly though, you’ll still need it for type manipulations).

In other words, you can write a compile time function that computes factorial in a straightforward way:

constexpr unsigned int factorial(unsigned int n)
{
return (n <= 1) ? 1 : (n * factorial(n - 1));
}

static constexpr auto magic_value = factorial(5);

constexpr first, ask question later

Now that you had a glimpse of what constant expression are, the question is : when should you use constexpr?

The answer is the same than with const: as much as you can.

This adds more compilation-time checks, gives the compiler more information to optimize better and this has the nice side effect of making you think more about what you are currently doing.

And remember: the longer it takes for your C++ program to compile, the greater your eliteness.

Top