Правилни дефиниции на препроцесора на C в CMake
Дефинициите на препроцесора често се използват в C++ проекти за условно компилиране на избрани секции от код, като специфични за платформата и т.н. В тази статия, очевидно, ще бъде разгледан единственият (но изключително труден за отстраняване на грешки) рейк, който можете да стъпите, когато поставите #defines чрез флагове на компилатор.
Като пример, нека вземем системата за изграждане на CMake, въпреки че същите действия могат да бъдат извършени във всеки друг от нейните популярни колеги.
Въведение и описание на проблема
Някои специфични за платформата дефиниции, като проверка за Windows/Linux, се предоставят от компилатора, така че да могат да се използват без допълнителна помощ от компилиращи системи. Въпреки това, много други проверки, като например наличието на #include файлове, наличието на примитивни типове, наличието на необходимите библиотеки в системата или дори просто определяне на битовостта на системата, са много по-лесни за извършване "отвън", като впоследствие се подават необходимите дефиниции през флаговете на компилатора. Като алтернатива можете просто да подадете допълнителни дефиниции:
В CMake, поставянето на #defines през компилатора се извършва с помощта на add_definitions, което добавя флагове на компилатора към целия текущ проект и неговите подпроекти, като почти всички команди на CMake:
Изглежда, че тук не може да има проблеми. Ако обаче не сте внимателни, можете да направите сериозна грешка:
Ако някои #define, предоставени от компилатора за проект A, се проверяват в заглавния файл на същия проект A, тогава, когато #include този заглавен файл от друг проект B, който не е подпроект на A, този #definenotще бъде поставен.
Пример 1 (прост)
Работен пример за описаната грешка може да бъдепогледнете github/add_definitions/wrong. Под спойлера, за всеки случай, се дублират значителни части от код:
Изпълнението на `exe` ще изведе:
Този пример е много прост: дори има някакъв изход към конзолата. В действителност такава грешка може да възникне при свързване на доста сложни библиотеки като Intel Threading Building Blocks, където някои от параметрите на ниско ниво наистина могат да бъдат предадени през дефиниции на предпроцесора и те също се използват в заглавни файлове. Намирането на невероятни грешки в такива условия е изключително болезнено и отнема много време, особено когато този нюанс на add_definitions не е бил виждан преди.
За по-голяма яснота, вместо два проекта, ще използваме един, вместо add_definitions ще има обикновен #define вътре в кода и ще откажем напълно CMake. Този пример е друга силно опростена, но реална ситуация, която представлява интерес, включително от гледна точка на общите познания на C ++.
Работещият код може да се види на github/add_definitions/cpphell. Както в предишния пример, значителни части от кода под спойлера:
Грешката е голяма. Единият файл (a.cpp) вижда членовете на класа, скрити под #ifdef, а другият (main.cpp) не. За тях класовете стават различни размери, което води до проблеми с управлението на паметта, по-специално Segmentation Fault:
Възможни решения
Не използвайте "външни" дефиниции в заглавните файлове
Ако е възможно, това е най-лесният вариант. За съжаление, понякога под #ifdefs има различни функционални сигнатури, специфични за платформата и компилатора, а някои библиотеки обикновено се състоят само от заглавни файлове.
Използвайте add_definitions в основния CMakeLists.txt
Това разбира се решава проблема със "забравените" предадени флагове за конкретен проект, нопоследствията са следните:
- Опциите на командния ред на компилатора ще включват всички флагове за всички проекти, включително онези проекти, които не се нуждаят от тези флагове - трудността при отстраняване на грешки, например чрез make VERBOSE=1, когато искате да разберете с какво този компилатор се самоизвиква в определен файл.
- Този проект не може да бъде "вграден" като подпроект в друг проект, защото тогава ще се наблюдава абсолютно същия проблем. Заслужава да се отбележи, че в CMake процесът на вграждане на проект най-често е напълно безболезнен и тази възможност често не трябва да се пренебрегва.
Използвайте конфигурационни заглавни файлове и configure_file
CMake предоставя възможност за създаване на конфигурационни заглавни файлове с помощта на configure_file. В хранилището се съхраняват предварително подготвени шаблони, от които в момента на изграждане на проекта от CMake се генерират самите конфигурационни файлове. Генерираните файлове са #включени в необходимите заглавни файлове на проекта.
Когато използвате configure_file, не забравяйте, че добавянето на дефиниции на препроцесор "извън" конкретен проект чрез add_definitions вече няма да работи. Разбира се, възможно е да се направи специален конфигурационен файл, който да задава флагове само ако вече не са зададени (#ifndef), но това само ще увеличи объркването.
Заключение
Hardcore conf в C++. Каним само професионалисти.
Чете сега
Наследяване в C++: начинаещи, средно напреднали, напреднали
Версия на CLion 2018.1: нови функции от C++17, поддръжка на WSL, CMake Install, плъгин за Rust и други
Версия на CLion 2016.3: подобрения в поддръжката на C11, C++11 и C++14, промени в модела на проекта CMake и други
Коментари 4
Честно казано, някои детски проблеми - как да не разберете, че обектите, използвани глобално, не могат да бъдат дефинирани локално? Това е проблемът и в двата примера.
Що се отнася до разрешаването на това в CMake, е зададено хубаво решение за библиотека, вградена в проект (MYLIB_DEFINITIONS "-DFOO" PARENT_SCOPE). Така че логиката за задаване на флаговете остава в библиотеката и родителският проект ги получава след add_subdirectory по същия начин, както обикновено се случва след find_package.
Решението configure_file е още по-надеждно - тъй като (както забелязахте) флаговете няма да висят в командите, няма да е възможно да забравите да ги активирате, но библиотеката във всеки случай ще трябва да премине към родителския MYLIB_INCLUDE_DIRS (изходни и двоични пътища към заглавни файлове - не забравяйте, че заглавката, генерирана след configure_file, отива в двоичната директория) и MYLIB_LIBRARY (тук само името на целта на библиотеката) със споменатия PARENT_SCOPE. Решението configure_file също е по-удобно по отношение на преместването на библиотеката в отделен проект - т.е. такъв, който може да бъде инсталиран в системата без промени чрез make install или включен директно в проекта, като за проекта разликата също ще бъде само един ред - find_package vs. add_subdirectory.
> Когато използвате configure_file, трябва да запомните, че сега поставянето на дефиниции на препроцесор "извън" конкретен проект чрез add_definitions няма да работи
И това не е, което трябва да направите. Библиотеката трябва да бъде конфигурирана чрез CMake - родителският проект трябва да задава само cmake променливи на високо ниво и да не влиза в флагове на компилатор на ниско ниво. Освен това, ако тези настройки наистина засягат флаговете, те ще бъдат върнати на родителя по описания по-горе начин.
Относно тривиалносттапроблеми - фактът е, че в случай на #defines в заглавките, поради липса на опит и/или невнимание, човек може да забрави, че обектите, оказва се, са глобални. Разбира се, втори път няма да направите такава грешка.
Иначе - мерси, ценна информация, напълно съм съгласен с теб.
cmake има повече от add_definitions
SET_TARGET_PROPERTIES($PROPERTIES COMPILE_FLAGS -DHELLO_WORLD) SET_SOURCE_FILES_PROPERTIES(main.cpp PROPERTIES COMPILE_FLAGS -DHELLO_WORLD) TARGET_COMPILE_DEFINITIONS/COMPILE_DEFINITIONS
Вашият пример 1 е много лесен за коригиране:
и без генериране на конфигурация по-отнемащ време начин чрез find_package и ръчно изработен Find*package name*.cmake