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. constexpr functions are also allowed to be called at runtime.

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 and Clang 14 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" can be used in constant expressions.

To store a text, use const char[] like this:

constexpr char TypeName[] = "Container";

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

std::string a{TypeName};

The memory usage of arrays are known at compile time, so, they are accepted as constexpr:

constexpr array<int,2> a{1,2};

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 along with Concepts are powerful tools for meta-programming.

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.

And finally, a constexpr data member must be static.

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

Note that when a constexpr variable is initialized with a constexpr function, we force the function to return a constexpr. If it doesn’t so, we get an error.

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*/}

constexpr vector and string

std::vector and std::string objects have a dynamic memory allocation, they cannot be persistent at both compile and runtime. There is no support for them before C++20, however, since then, these objects are allowed to be in places like constexpr functions, so they are destructed at the end of compile time.

You get errors doing this:

int main(){
     constexpr std::string s{"Hi"}; // Error
     constexpr vector<int> v{1,2}; // Error
     return 0;
}

However this is correct and supported in GCC 12 with flag -std=c++20:

constexpr auto g(){
    auto s = string{"hello"};
    
     vector<int> v{1,2};
     v.push_back(3);
     
     auto sum=0;
     for (auto& n : v)
        sum += n;

     return sum;
}

int main(){
    // forcing computation g() at compile time
    auto constexpr x = g();
    return 0;
}

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 that is used at runtime, now you can also employ it at compile time by just adding constexpr specifier to it.

constexpr vs static constexpr

At the global scope, both are the same and static keyword is not necessary. However, in a function scope, their behavior is compiler-dependent.

In this example:

#include<array>

auto get(int n)
{
     constexpr std::array<char,4> 
                    a{'1','2','3','4'};
    return a[n];
}

GCC copies the array to the stack every time the function is called. But Clang creates a table of the array in the compiled program without the need for copying into the stack.

If you change constexpr to static constexpr, both GCC and Clang create a table of data which is mostly desired. Therefore, in the function scope, we better use static constexpr.

Debug

While you may find some tricks on the internet, there is no standard debugging system for compile-time programming. Therefore, for complex calculations, the constexpr functions are better tested first at runtime and 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 sequences. 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;
}

References

cppreference, Scott Schurr Youtube, Rainer Grimm Youtube, stackoverflow, Jason Turner Youtube, Jason Turner Youtube.

Subscribe

I notify you of my new posts

Latest Posts

Comments

1 comment
Falk Heinrich 12-Jul-2023
Thanks a lot. This summary of "constexpr" gave me the full picture. Excellent documentation.