C++ constexpr makes compile-time programming a breeze

Introduction

Constant expression, constexpr, code in C++ aims to move non-changing repetitive computations at runtime to compile-time. For example, you can write a function that calculates π² at compile-time, so, whenever you run the program, π² value is already there. However, constexpr is not used in the part of the code that depends on runtime inputs (spoiler alert, functions are a bit tricky!).

Prerequisites

Here I assume you are familiar with the concept of compile-time vs runtime, auto keyword, and templates.

I am using GCC 10.2 with flag -std=c++20.

constexpr values

We can store a compile-time value with constexpr:

constexpr int i = 0;

Note that if you drop constexpr the variable is set at runtime and is not suitable for compile-time programming.

Therefore, something like this is accepted:

constexpr int i=5;
int j=i;

but this is NOT accepted:

int i=5; // set at runtime
constexpr int j=i; //error i is not constexpr to set j

So you can use constexpr values at runtime but you cannot use runtime variables at compile-time.

constexpr variable neither can be changed at compile-time nor runtime:

constexpr int k=1;
k=2; // Error

To set a constexpr variable based on some logic, a constexpr function is employed. It is explained in the constexpr function section.

And probably you already noticed that literals like 1,2.5 and "hello" are constexpr.

Since C++20, std::string can be constant expression. For lower standards, use const char[] instead like:

constexpr char TypeName[] = "Container";

And if you need a string, a runtime one can be made:

std::string a{TypeName};

const vs constexpr

const and constexpr variables are immutable. constexpr variables are initialized at compile-time, however, initialization of const variables will be delayed to runtime if a compiler decides so.

This is an error:

int i =1;
const int j=i; // j is runtime const
constexpr int k=j; // Error

However, this is successfully compiled with GCC 10.2:

const int j=1; // set at compile-time
constexpr int k=j;

Therefore, for clarity and avoiding bugs, instead of const use constexpr variables for compile-time programming.

constexpr and template

Like literals, constexpr variables can be used to instantiate a template function or class.

template <int size>
class Book{/*uses size*/};

constexpr int mySize=10;

Book<mySize> book;

In the above example, drop constexpr to see what error you get.

constexpr class

The object of a class can participate in constexpr statement if it has a constexpr constructor:

class Book{
    public:
    int pageCount;
    constexpr Book(int n):pageCount(n){}
};

constexpr Book b{100};
int pages[b.pageCount]; 

If a member function needs to be used in constexpr statement, it must be constexpr too:

class Book{
    int authorsCount;
    public:
    constexpr Book(int n):authorsCount(n){}
    constexpr auto GetAuthorsCount() const{ 
        return authorsCount;}
};

constexpr Book b{5};
int pages[b.GetAuthorsCount()];

Note, in the member function definition, the qualifier of this must be const.

if constexpr

To decide a condition at compile, if constexpr is used:

if constexpr (a_condition)
    /* do something */;
else if constexpr (b_condition)
    /* do another  thing */;
else
    /* do default */;

Do not miss constexpr for else if.

The conditions must be evaluated from a constant expression. Therefore, this is correct

constexpr int i=10;
if constexpr(i==10)
    std::cout<<"hi";

But this NOT OK:

int i=10;
if constexpr(i==10) // Error: i is not constant expression 
    std::cout<<"hi";

The final compiled program doesn’t include the sections of unmet conditions:

constexpr int i=10;
if constexpr (i==10)
    std::cout<<"hi";
else 
    std::cout<<"bye";

The above code compiles as if we had

constexpr int i=10;
std::cout<<"hi";

It can be a powerful tool for eliminating a part of the code that must not be included at runtime.

constexpr function

We can have a function that is evaluated at compile-time:

auto constexpr f(int x){
    return x+1;
}

and somewhere else you can write:

constexpr int y=f(2); //y=3

You can have more logic evaluated at compile-time, like calculating Pi number:

auto constexpr f(int n){
   double sum = 0.0;
    int sign = 1;
    for (int i = 0; i < n; ++i) {           
        sum += sign/(2.0*i+1.0);
        sign *= -1;
    }
    return 4.0*sum;
}

use it like

constexpr auto x=f(5);

However, a constexpr function turns to a runtime (non-constexpr) function if arguments are not constexpr:

int i=5;
auto x=f(i); // OK: evaluated at runtime

and you get an error if you do this:

int i=5;
constexpr auto x=f(i); // f is not evaluated with a constexpr

constexpr function parameter

Currently, there is no support for a constexpr function parameter:

// NOT supported code
auto f(constexpr int i){/*Use i as constexpr*/}

However, there is a proposal for this feature, see P1045.

Therefore, for now, the only choice is to use a template:

template<int i>
auto f(){/*Use i as constexpr*/}

consteval vs constexpr function

consteval function mandates the function returns a compile-time constant but constexpr function, besides compile-time, can also be called at runtime.

consteval auto g(int n){
    return n*n;
}

constexpr auto x=g(1); // OK

int j=1;
auto y=g(j); // Error: g cannot make constexpr with j

However, a constexpr function can be called at both compile-time and runtime:

constexpr auto f(int n){
    return n*n;
}

constexpr auto x=f(1); // OK: compile-time call

int j=1;
auto y=f(j); // OK: runtime call

Therefore, if the function must be evaluated at compile-time only, use consteval and you get an error from the compiler if otherwise happens. However, if you already have a function in your code used at runtime, now you can also use it at compile-time by just adding constexpr specifier to it.

Debug

While you may find some tricks on the internet, there is no standard debugging system for compile-time programming. Therfore, for complex calculations, they are better tested first at runtime then used at compile-time.

Constexpr limit

Because there is no debugging for compile-time cacluations, there are some limits on constexpr operations and loops. They can be edited via compiler flags: -fconstexpr-depth=n, -fconstexpr-loop-limit=n, -fconstexpr-ops-limit=n.

Case study 1

A program always needs 20th number of Fibonacci sequence. Can you write a function that calculates this at compile-time, so we don’t waste time computing this value at runtime?

Solution

constexpr int fib(int n)
{
    if (n <= 1)
        return n;
    return fib(n-1) + fib(n-2);
}
 
constexpr auto fib20 = fib(20);

Case study 2

In C++, you cannot overload a function based on its return type. So this will NOT compile:

int g(int i){
    return 10;
}
std::string g(int i){ //Error: ambiguous
    return std::string("hello");
}

Can you write a function using meta-programming that returns a string if a condition is met and otherwise it returns an int?

Solution:

#include <iostream>


template<int i>
auto f(){
    if constexpr(i==0)
        return 10;
    else
        return std::string("hello");
}

int main(){

  std::cout<<f<0>(); // 10
  std::cout<<f<1>(); // hello

}

Note that while at compile-time they are the same, but at runtime, f<0> and f<1> are two different animals with different signatures.

Case study 3

Imagine we have a 3D box filled with ping-pong balls, we store each ball as

struct point3d{
    int location[3];
}

But we know if the thickness of the box is equal to the diameter of the ball, the model can be 2D

struct point2d{
    int location[2];
}

In usual programming, we have to write two libraries to handle 2D balls and 3D balls separately:

print3d(point3d& p){
    for (int i=0; i<3; i++)
        std::cout<<p.location[i];
}
print2d(point2d& p){
    for (int i=0; i<2; i++)
        std::cout<<p.location[i];
}

But is there a way that, keeping the same structure (POD), we write one piece of code that if the box is very thin, it is compiled to a 2D balls program otherwise 3D?

See the solution:

#include <iostream>

constexpr auto GetDimensions(double boxThickness, 
                             double ballDiameter){

  if (boxThickness>2*ballDiameter)
    return 3;
  else if (boxThickness > ballDiameter)
    return 2;
  else 
    return 0;
}

template<int size>
struct ball{
    int location[size]={};
};

template<int size>
auto print(ball<size>& p){
    for (int i=0;i<size;i++)
        std::cout<<p.location[i];
}

// Change box thickness to 5, 15 and 30
// which leads to error, 2D and 3D compilation
constexpr double boxThickness=30;
constexpr double ballDiameter=10;

constexpr int dim = GetDimensions(boxThickness,ballDiameter);
static_assert(dim, "Dim must be > 0");

int main(){

    ball<dim> p;

    print(p); // 00 for 2D and 000 for 3D

    return 0;
}

Related Tags


Latest Posts