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
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.
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.
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.
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.