Compile-time vs runtime programming in C++

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++, 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 like std::any for runtime dynamic types which I will talk about in another post.

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.

(.Get 1)

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 to double for velocity 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 like velocity[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:

(.Get 1)

If the code is compiled again with different T and i, we get a different executable:

(.Get 1)

Related Tags


Latest Posts