Определения препроцессора часто используются в 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++
-
Как Обратиться В Сервисный Центр
19 Oct, 24 -
Росновский Парк™ Еженедельный Подкаст №105
19 Oct, 24