License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Hervé Frezza-Buet, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : preprocessing.md

Table of contents

Pre-processing

Compiling consists of transforming your C++ code, i.e. a text file, into a binary executable file, that you may never want to read. Indeed, the compiling chain of C++ inserts a supplementary translation in the process, named “pre-processing”.

This phase consists of taking your C++ text files, your code, and slightlty rewriting them before the actual translation into binary code occurs. Strictly speaking, the C++ code that is compiled is not exactly the one that you have written !

Nobody takes care of this, but in this section, we will explicit the file that is pre-processed, since understanding this mechansim is crucial to understand how C++ code is structured.

The preprocessor directives

Get the preprocess-001.tar.gz archive, uncompress it and dive into the directory.

mylogin@mymachine:~$ tar zxvf preprocess-001.tar.gz
mylogin@mymachine:~$ rm preprocess-001.tar.gz
mylogin@mymachine:~$ cd preprocess-001

The code starting with # is preprocessing directives. Read in that order definitions.cpp, functions.cpp and main.cpp.

The main code is main.cpp, from which we aim at building an executable binary file. Let us build (i.e. compile) an executable from main.cpp. It will fail, but try it.

mylogin@mymachine:~$ g++ -o my_software main.cpp

Ok. Here, the preprocessor has transformed your main.cpp into another C++ file, and it has compiled this latter file… and an error occurred (redefinition of struct Complex…).

Let us see the file obtained after pre-processing… this is something that nobody does usually. The preprocessor is names cpp.

mylogin@mymachine:~$ cpp main.cpp main-preprocessed.cpp

It has generated main-preprocessed.cpp, we will edit it later. For now, if you compile the preprocessed file, you will see exactly the same error as previously, since it is the same compiling.

mylogin@mymachine:~$ g++ -o my_software main-preprocessed.cpp

It is time to edit main-preprocessed.cpp (do it now). The directive #include means “copy the content of the file here”, #define defines macros that are replaced by their values by the pre-processor, and #ifdef triggers the reading of some code according to the existance or not of some previous definition. In the file resulting from preprocessing, i.e. main-preprocessed.cpp, there are some # n ... lines, n being a line number. Ignore these, they are some kind of comments.

Knowing these rules, re-read definitions.cpp, functions.cpp and main.cpp and you may understand the relation between main-preprocessed.cpp and the 3 files we had initially.

Note that definitions.cpp is copied twice during the process… which leads to the error.

Let us define, from command line, the macro HIGH_PRECISION. Check the generated main-preprocessed.cpp file to see the effect (on values of pi).

mylogin@mymachine:~$ cpp main.cpp -DHIGH_PRECISION main-preprocessed.cpp

Last, in order to avoid the copy-pasting of definitions.cpp twice, it should be nice to mention that if the file has already been copied into the final result during pre-processing, it should not be copied by next #include directives.

Add the line #pragma once at the beginning of the definitions.cpp file. preprocess main-preprocessed.cpp, edit it to check the effect (you should see now the Complex class definition only once), and compile it.

mylogin@mymachine:~$ cpp main.cpp main-preprocessed.cpp
mylogin@mymachine:~$ g++ -o my_software main-preprocessed.cpp

You can execute the generated program…

mylogin@mymachine:~$ ./my_software

It displays nothing… but it has done the job. Once again, preprocessing is done implicitly, and a usual compiling is directly:

mylogin@mymachine:~$ g++ -o my_software main.cpp

There is no error now, thanks to the #pragma once, but solving the issue required to understand the implicit preprocessing mechanism.

The classical usage of preprocessing directives

Trigger debug mode

Having a code whith conditional compiling as

int my_function(...) {
#ifdef DEBUG
    std::cout << "Entering my function" << std::endl;
#endif
}

allows for adding -DDEBUG to your compiler flags so that the preprocessor writes a code with the debugging messages. When you recompile without the -DDEBUG, a new code, free from the printing of the messages, is generated. There is no test at execution time of the value of some debugging flag, which saves time when debugging is disabled.

Using header files and separate the compiling

Function header and type definition may lie in header files, that can be used (i.e. included) by any piece of code that needs the definition. This is the role of header files, suffixed by .hpp for example.

We have included .cpp files in the previous example, but this is not the classical use.

Indeed programmers usually include header files, i.e .hpp files, containing definitions.

Such file may include other ones, so inclusions form a recursive process, as we did for functions.cpp that included definitions.cpp. This is why it is recommended to start all of your .hpp files with the #pragma once directive, so that any user includes the file s/he thinks s/he needs, and does not wonder about multiple definitions due to recursive inclusions.

We provide an example in the compiling section of this site.

Hervé Frezza-Buet,