Preprocessor definitions (preprocessor definitions) are often used in C ++ projects for conditional compilation of selected sections of code, for example, platform-specific, etc. This article will consider, apparently, the only (but extremely difficult to debug) rake, which can be stepped on by defining # define through compiler flags.
As an example, take the
CMake build system, although the same actions can be performed in any of its other popular counterparts.
Introduction and description of the problem
Some platform-specific definitions, such as testing on Windows / Linux, are put down by the compiler, so they can be used without additional help from build systems. However, many other checks, such as the presence of #include files, the presence of primitive types, the presence of required libraries in the system, or even a simple definition of the system’s bitness, are much easier to do outside, passing the required definitions through the compiler flags. In addition, you can simply pass additional definitions:
An example of passing # define through compiler flagsg++ myfile.cpp -D MYLIB_FOUND -D IOS_MIN_VERSION=6.1
#ifdef MYLIB_FOUND #include <mylib/mylib.h> void DoStuff() { mylib::DoStuff(); } #else void DoStuff() { // own implementation } #endif
In CMake, setting # define through the compiler is done using
add_definitions , which adds compiler flags to the entire current project and its subprojects, like almost all CMake commands:
')
add_definitions(-DMYLIB_FOUND -DIOS_MIN_VERSION=6.1)
It would seem that there can be no problems here. However, with carelessness, you can make a serious mistake:
If some #define supplied by the compiler for project A is checked in the header file of the same project A, then if #include this header file from another project B, which is not a subproject A, this #define will
not be affixed.
Example 1 (simple)
A working example of the error described can be viewed at
github / add_definitions / wrong . Under the spoiler, just in case, significant pieces of code are duplicated:
add_definitions / wrong project(wrong) add_subdirectory(lib) add_subdirectory(exe)
project(lib) add_definitions(-DMYFLAG=1) add_library(lib lib.h lib.cpp)
project(exe) add_executable(exe exe.cpp) target_link_libraries(exe lib)
Running `exe` will output:
foo: you're screwed :( bar: all good!
This example is very simple: it even has some kind of output to the console. In reality, such an error can occur when connecting sufficiently sophisticated libraries like
Intel Threading Building Blocks , where part of the low-level parameters can actually be passed through preprocessor definitions, and they are also used in header files. The search for amazing mistakes in such conditions is extremely painful and long, especially when this nuance of add_definitions has not been encountered before.
Example 2
For clarity, instead of two projects we will use one, instead of add_definitions, there will be an ordinary #define inside the code, and we will refuse from CMake. This example is another greatly simplified, but real situation that presents interest, including from the point of view of general knowledge of C ++.
The running code can be viewed on
github / add_definitions / cpphell . As in the previous example, significant parts of the code under the spoiler:
add_definitions / cpphell The mistake is beautiful. One file (a.cpp) sees the class members hidden under # ifdef-ohm, and the other (main.cpp) does not. For them, classes become of different sizes, which causes problems with memory management, in particular, Segmentation Fault:
g++ main.cpp a.cpp -o main.out && ./main.out
sizeof(A) in main.cpp = 1 sizeof(A) in A constructor = 32 sizeof(A) in A destructor = 32 Segmentation fault incoming... Segmentation fault (core dumped)
If you uncomment verbose_segfault () in main.cpp, you will see at the end:
*** Error in `./main.out': free(): invalid next size (fast): 0x000000000149f010 *** ======= Backtrace: ========= ... ======= Memory map: ======== ...
After a number of experiments, it turned out that if instead of the STL classes we used any number of primitive types in the fields of class A, then there were no drops, since destructors are not called for them. In addition, if you insert a single std :: string (on 64-bit Arch Linux and GCC 4.9.2 sizeof (std :: string) == 8), then there is no fall, and if you have two, you already have it. I think the point is alignment, but I hope that in the comments they will be able to explain in detail what is actually happening.
Possible solutions
Do not use "external" definitions in header files.
If this is possible, then this is the easiest option. Unfortunately, sometimes under # ifdefs there are various platform and compiler-dependent function signatures, and some
libraries generally consist only of header files.
Use add_definitions in the root CMakeLists.txt
This, of course, solves the problem of “forgotten” flags for a specific project, but the consequences are as follows:
- The command line parameters of the compiler will include all flags for all projects, including those projects that do not need these flags - difficulty in debugging, for example, through make VERBOSE = 1, when you want to understand what this compiler causes on a particular file.
- This project cannot be “embedded” as a subproject in another project, because then exactly the same problem will be observed. It is worth noting that in CMake the process of embedding a project, most often, is completely painless, and this possibility is often not worth neglecting.
Use configuration header files and configure_file
CMake provides the ability to create configuration headers with
configure_file . Pre-prepared templates are stored in the repository, from which, at the time of building the CMake project, the configuration files themselves are generated. Generated # files are included in the required project header files.
When using configure_file, it should be remembered that now putting the preprocessor definitions “outside” of a specific project via add_definitions will not work. Of course, you can make a special configuration file, which puts down flags only if they have not yet been affixed (#ifndef), but this will make even more confusion.
Conclusion
The errors and solutions shown are, of course, suitable not only for CMake projects, but also for projects with other build systems.
I hope this article will save someone a lot of time once when debugging completely magical errors in C ++ projects that contain pre-processor definitions in header files.