Правильная Маркировка Определений Препроцессора C++ В Cmake.

Определения препроцессора часто используются в C++-проектах для условной компиляции выбранных участков кода, например, платформозависимых и т.п.

В данной статье будут рассмотрены, видимо, единственные (но крайне сложные для отладки) грабли, на которые можно наступить при постановке #определяется с помощью флагов компилятора.

Возьмем в качестве примера систему сборки.

CMake , хотя те же действия можно выполнить и в любом другом популярном аналоге.



Введение и описание проблемы

Некоторые определения, специфичные для платформы, такие как проверка Windows/Linux, предоставляются компилятором, поэтому их можно использовать без дополнительной помощи со стороны систем сборки.

Однако многие другие проверки, такие как наличие #include файлов, наличие типов-примитивов, наличие в системе необходимых библиотек или даже просто определение разрядности системы, гораздо проще сделать «снаружи», впоследствии передача необходимых определений через флаги компилятора.

Альтернативно вы можете просто передать дополнительные определения: Пример передачи #defines через флаги компилятора

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

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

В CMake размещение #defines через компилятор выполняется с помощью add_definitions , который добавляет флаги компилятора ко всему текущему проекту и его подпроектам, как и почти все команды CMake:

add_definitions(-DMYLIB_FOUND -DIOS_MIN_VERSION=6.1)

Казалось бы, здесь не может быть никаких проблем.

Однако если не быть внимательным, можно совершить серьезную ошибку: Если некоторый #define, предоставленный компилятором для проекта A, проверяется в заголовочном файле того же проекта A, то при #include этого заголовочного файла из другого проекта B, который не является подпроектом A, этот #define Нет будет отмечено.



Пример 1 (простой)
Рабочий пример описанной ошибки можно найти по адресу github/add_definitions/неправильно .

Под спойлером на всякий случай дублируются значимые куски кода: add_definitions/неверно

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(); }

Запуск `exe` выведет:

foo: you're screwed :( bar: all good!

Этот пример очень простой: в нем даже есть какой-то консольный вывод. В реальности такая ошибка может возникнуть при подключении довольно сложных библиотек типа Строительные блоки Intel Threading , где некоторые параметры низкого уровня фактически могут передаваться через определения препроцессора, а также используются в файлах заголовков.

Обнаружение неожиданных ошибок в таких условиях крайне болезненно и отнимает много времени, особенно когда с этим нюансом add_definitions раньше не сталкивались.



Пример 2
Для наглядности вместо двух проектов мы будем использовать один, вместо add_definitions внутри кода будет обычный #define, а от CMake мы вообще откажемся.

Этот пример — еще одна сильно упрощенная, но реальная ситуация, представляющая интерес, в том числе и с точки зрения общих знаний C++.

Работающий код можно просмотреть по адресу github/add_definitions/cpphell .

Как и в предыдущем примере, важные участки кода находятся под спойлером: add_definitions/cpphell

// a.h 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 "a.h" 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 "a.h" // 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; }

Ошибка замечательная.

В одном файле (a.cpp) члены класса скрыты под #ifdef, а в другом (main.cpp) — нет. Для них классы становятся разного размера, что приводит к проблемам с управлением памятью, в частности, 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)

Если вы раскомментируете вызов verbose_segfault() в main.cpp, то в конце вы увидите:

*** Error in `.

/main.out': free(): invalid next size (fast): 0x000000000149f010 *** ======= Backtrace: ========= .

======= Memory map: ======== .



После некоторых экспериментов выяснилось, что если вместо классов STL использовать любое количество типов-примитивов в полях класса А, то сбоя не происходит, так как деструкторы для них не вызываются.

Кроме того, если вы вставите один std::string (в 64-битной версии Arch Linux и GCC 4.9.2 sizeof(std::string) == 8), то сбоя не произойдет, а если их два, то это уже крах.

Я предполагаю, что это проблема с согласованием, но надеюсь, что комментарии смогут более подробно объяснить, что на самом деле происходит.

Возможные решения



Не используйте «внешние» определения в файлах заголовков.

Если есть возможность, то это самый простой вариант. К сожалению, иногда #ifdefs содержат разные сигнатуры функций, зависящие от платформы и компилятора, а некоторые библиотеки обычно состоят только из заголовочных файлов.



Используйте add_definitions в корневом файле CMakeLists.txt.
Это, конечно, решает проблему «забытых» переданных флагов для конкретного проекта, но последствия следующие:
  • В параметры командной строки компилятора будут включены все флаги для всех проектов, включая те проекты, которым эти флаги не нужны — сложно отлаживать, например, через make VERBOSE=1, когда хочется понять, почему этот компилятор вызывает себя на конкретный файл.

  • Этот проект нельзя «встроить» как подпроект в другой проект, потому что тогда возникнет точно такая же проблема.

    Стоит отметить, что в CMake процесс внедрения проекта, чаще всего, совершенно безболезненный, и пренебрегать этой возможностью зачастую не стоит.



Используйте файлы заголовков конфигурации и configure_file.
CMake предоставляет возможность создавать файлы заголовков конфигурации, используя конфигурационный_файл .

В репозитории хранятся заранее подготовленные шаблоны, из которых в момент сборки проекта CMake генерируются сами файлы конфигурации.

Сгенерированные файлы #include в необходимые файлы заголовков проекта.

При использовании configure_file следует помнить, что теперь поместить определения препроцессора «вне» конкретного проекта через add_definitions не получится.

Конечно, можно сделать специальный файл конфигурации, который будет устанавливать флаги только в том случае, если они еще не установлены (#ifndef), но это добавит еще больше путаницы.



Заключение

Показанные ошибки и решения, разумеется, подходят не только для проектов CMake, но и для проектов с другими системами сборки.

Я надеюсь, что эта статья однажды сэкономит кому-нибудь массу времени при отладке совершенно магических ошибок в проектах C++, которые включают определения препроцессора в заголовочные файлы.

Теги: #C++ #препроцессор #определения #CMake #разработка веб-сайтов #C++

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.