Introduction
C++ is a statically typed language in which the type of variables is set at compile-time. Some other examples are C#, Java and Fortran. In contrast to them, we have higher-level languages like Python and JavaScript which are dynamically typed. For example, in Python this is acceptable:
i=1 #i is integer
i="hello" #i is now string
But in C++, we have to specify the type of variables and it is not changing at runtime. This is an ERORR
int i;
std::string i;
C++ is mostly picked because of high speed in computation. The language is a low level one very close to the machine language. It gives the programmer more tools to optimise the code than Python, Java or C#.
To partially overcome the burden of being statically typed language, a new set of syntax and commands was invented that lets us program the types and constant values for just compile-time.
There are higher-level structures in C++ like std::any
for runtime dynamic types, but they heavily degrade the performance of the code.
Definitions
Runtime programming is writing the code with the understanding that the logic will be executed when the compiled program is run. Probably, this type of programming is enough to know for 90% of general C++ projects.
Compile-time programming is to ask the compiler to change the source code based on some logic and then create an executable. Applying a pinch of it can facilitate many problems, however, using too much of it can make a project very hard to read and downgrade productivity.
To compare compile-time and runtime, the stages to create and run a program are explained with an example:
1- write and store the code
//myProgram.cpp
#include <iostream>
int main(){
int i=5;
std::cout<< i;
return 0;
}
2- Compile the code
In a Linux (Mac/windows) terminal, you can compile the code
c++ myProgram.cpp -o myProgram.out
Anything that happens after running the above line is a compile-time process. At the end of compilation, you get an executable, myProgram.out
.
3- Execute the code
By running the program:
./myProgram.out
We start run-time stage. i
is defined and initialized in the memory and then printed on the screen.
With meta-programming, we can have a compiler to run some logic for us at stage 2. To show the use cases see the below example:
struct Particle{
float velocity[2];
auto run(bool b){
if (b)
// do something
else
// do another thing
}
};
If this structure is compiled in C++, there is no way to change it during runtime. However, we can change it at compile-time to satisfy the end-user need:
Changing the precision of the code. In the example, we would be able to switch
float
todouble
forvelocity
based on some conditions.Removing some sections of the code from executable. In the example, imagine, during a whole run,
b
is a constant being true or false. The code would be faster if at compile time we cut the section which won’t be called at all. Another example is when having compiler (GCC/Clang/MSVC/Intel) dependent code where one is included and the rest are removed.Changing data structures. In the example, we would have
velocity[2]
size to be dependent on a compile-time variable likevelocity[N]
.
Macros
C++ meta-programming used to be written with macro commands starting with #
. We could write the previous examples like
#define T float
#define GETSIZE(x) (x*x)
struct Particle{
T velocity[GETSIZE(2)];
};
Before compilation, a pre-processor, like a text editor, replaces every GETSIZE(x)
in the source code with (x*x)
. C++ inherited macros from C, they can make the code look unpleasant and harder to debug and maintain. Nowadays, the only macros that I use in my codes are #include
and header guards. Hopefully, we will put those aside too when major compilers fully support modules
.
Compile-time programming
From C++11 many familiar C++-style tools are created for meta-programming like if constexpr
, constexpr
variables, constexpr
functions, concepts, static_assert()
, and meta-functions of type_traits
library.
For instance, the previous code can be written with constexpr
function like:
using T=float;
constexpr int i=2;
constexpr auto GetSize(int x) {return x*x;}
struct Particle{
T velocity[GetSize(i)];
};
Compiling the code, we get an executable through this process:
If the code is compiled again with different T
and i
, we get a different executable:
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