📜 ⬆️ ⬇️

Correct insertion of C ++ preprocessor definitions in CMake

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 flags
g++ 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) 


 // lib.h static void foo() { #ifdef MYFLAG std::cout << "foo: all good!" << std::endl; #else std::cout << "foo: you're screwed :(" << std::endl; #endif } void bar(); // implementation in lib.cpp 

 // lib.cpp #include "lib.h" void bar() { #ifdef MYFLAG std::cout << "bar: all good!" << std::endl; #else std::cout << "bar: you're screwed :(" << std::endl; #endif } 

 // exe.cpp #include "lib/lib.h" int main() { foo(); bar(); } 


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
 // ah class A { public: A(); // implementation in a.cpp with DANGER defined ~A(); // for illustrational purposes #ifdef DANGER std::vector<int> just_a_vector_; std::string just_a_string_; #endif // DANGER }; 

 // a.cpp #define DANGER // let's have a situation #include "ah" A::A() { std::cout << "sizeof(A) in A constructor = " << sizeof(A) << std::endl; } A::~A() { std::cout << "sizeof(A) in A destructor = " << sizeof(A) << std::endl; std::cout << "Segmentation fault incoming..." << std::endl; } 

 // main.cpp #include "ah" // DANGER will not be defined from here void just_segfault() { A a; // segmentation fault on 'a' destructor } void verbose_segfault() { A *a = new A(); delete a; } int main(int argc, char **argv) { std::cout << "sizeof(A) in main.cpp = " << sizeof(A) << std::endl; // verbose_segfault(); // uncomment this just_segfault(); std::cout << "This line won't be printed" << std::endl; } 


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:


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.

Source: https://habr.com/ru/post/254365/


All Articles