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.
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks