Конфігурації збірки: Debug, Release і вирази-генератори
Конфігурації збірки: Debug, Release і вирази-генератори
🎯 Мета лекції
- Розрізняти Debug, Release, RelWithDebInfo та MinSizeRel і розуміти, які прапорці вони додають.
- Встановлювати тип збірки з командного рядка та задавати значення за замовчуванням.
- Налаштовувати прапорці й макроси окремо для кожної конфігурації.
- Знати, чому на Visual Studio і на Makefiles конфігурація задається по-різному.
🔑 Ключові терміни
- Конфігурація (configuration, тип збірки): набір прапорців компілятора для певної мети.
- Оптимізація (optimization): перетворення коду компілятором для швидкості або розміру.
- Debug-інформація: таблиці відповідності машинного коду й рядків вихідного файлу.
- Вираз-генератор (generator expression): запис
$<...>, який обчислюється при генерації файлів збірки.
Одну й ту саму програму можна скомпілювати двічі: для розробника і для користувача. У першому випадку потрібен відладчик і зрозумілі повідомлення про помилки, у другому швидкість. Це різні конфігурації, і CMake вміє перемикатися між ними без змін коду.
Що таке конфігурації збірки
CMake має чотири стандартні конфігурації:
| Конфігурація | Призначення | Оптимізація | Відладочна інформація |
|---|---|---|---|
Debug | Розробка й налагодження | Немає | Є |
Release | Випуск для користувачів | Максимальна | Немає |
RelWithDebInfo | Випуск, який можна налагоджувати | Помірна | Є |
MinSizeRel | Мінімальний розмір файлу | На розмір | Немає |
- Debug: програма містить інформацію для відладчика, тому можна ставити точки зупинки й бачити значення змінних. Оптимізація вимкнена, тому компілюється швидко, але працює повільніше. Файл більший.
- Release: максимальна оптимізація. Швидка програма меншого розміру, але налагоджувати важко: компілятор міг змінити порядок і викинути змінні.
- RelWithDebInfo: оптимізація плюс відладочна інформація. Зручно, коли помилка проявляється лише у Release-збірці, і потрібно отримати стек викликів.
- MinSizeRel: оптимізація на розмір. Використовується для вбудованих систем і там, де обмежено пам'ять.
Які прапорці додає CMake насправді
Конфігурація — це лише назва, яка вмикає відповідну змінну CMAKE_CXX_FLAGS_<КОНФІГУРАЦІЯ>. Перевіримо, які прапорці виходять на практиці. Один і той самий проєкт зібрано чотири рази (компілятор AppleClang, прапорці з make VERBOSE=1):
Звідси кілька висновків:
- У Debug немає
-O0: оптимізація вимкнена тим, що вона просто не додається (на GCC та Clang рівень за замовчуванням і є-O0). - У всіх конфігураціях, крім Debug, визначено макрос
NDEBUG. Саме він вимикаєassert()зі стандартної бібліотеки. -O3,-O2,-Osзадають рівні оптимізації: найшвидший, помірний та на розмір.
Для MSVC набори інші (/Od /Zi /RTC1 у Debug, /O2 /DNDEBUG у Release), але ідея та сама.
NDEBUG у Release-збірці всі виклики assert() зникають із програми. Тест, написаний на assert, у Release завжди «проходить», бо перевірок немає. Тому запускайте такі тести в Debug-збірці або користуйтеся тестовим фреймворком (Google Test, Catch2), у якому перевірки не залежать від NDEBUG.Як вибрати конфігурацію
З командного рядка
Для генераторів з однією конфігурацією (Makefiles, Ninja) тип вибирається на етапі конфігурації:
# Debug
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
# Release
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# Release з відладочною інформацією
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
# Мінімальний розмір
cmake -S . -B build -DCMAKE_BUILD_TYPE=MinSizeRel
CMAKE_BUILD_TYPE не вказано, змінна залишається порожньою. Тоді прапорці конфігурацій не додаються взагалі: збірка не оптимізована і не має відладочної інформації. Це четвертий, «безіменний» режим, який легко сплутати з Debug.У CMakeLists.txt: значення за замовчуванням
cmake_minimum_required(VERSION 3.10)
project(MyApp)
# Якщо тип не вказано, використати Debug
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
message(STATUS "Build type not specified, using Debug")
endif()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
add_executable(MyApp main.cpp)
У команді set(... CACHE STRING "..." FORCE) значення записується в кеш, тому його видно у cmake -L і GUI. Слово FORCE потрібне, щоб перезаписати порожнє значення, що вже є в кеші.
CMAKE_BUILD_TYPE не використовується, а тип обирається при збірці. Для таких генераторів перевірте CMAKE_CONFIGURATION_TYPES або оберіть підхід із виразами-генераторами нижче.Налаштування для кожної конфігурації
Вирази-генератори
Вираз-генератор записується як $<...> і обчислюється при генерації файлів збірки, коли відомо, яка саме конфігурація збирається. Вираз $<$<CONFIG:Debug>:значення> читається так: «якщо конфігурація Debug, то це значення, інакше нічого».
cmake_minimum_required(VERSION 3.10)
project(MyApp)
set(CMAKE_CXX_STANDARD 17)
add_executable(MyApp main.cpp)
# Загальні попередження залежно від компілятора
target_compile_options(MyApp PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<CXX_COMPILER_ID:GNU>:-Wall;-Wextra>
$<$<CXX_COMPILER_ID:Clang,AppleClang>:-Wall;-Wextra>
)
# Тільки для Debug
target_compile_definitions(MyApp PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
)
target_compile_options(MyApp PRIVATE
$<$<AND:$<CONFIG:Debug>,$<NOT:$<CXX_COMPILER_ID:MSVC>>>:-O0;-g>
)
# Тільки для Release
target_compile_options(MyApp PRIVATE
$<$<AND:$<CONFIG:Release>,$<NOT:$<CXX_COMPILER_ID:MSVC>>>:-O3>
)
Кілька прапорців усередині виразу розділяються крапкою з комою (-Wall;-Wextra), бо вираз має повернути список. Складні умови будуються з $<AND:...>, $<OR:...>, $<NOT:...>.
-DNDEBUG та -O3 для Release CMake додає сам. Ваша задача — додавати те, чого немає за замовчуванням: макроси проєкту, санітайзери, додаткові попередження.Умова if(CMAKE_BUILD_TYPE ...)
Альтернатива виразам — звичайний if:
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Configuring for Debug")
target_compile_definitions(MyApp PRIVATE DEBUG_LOGGING_ENABLED)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "Configuring for Release")
target_compile_definitions(MyApp PRIVATE RELEASE_BUILD)
endif()
Цей варіант простіший для читання, але працює лише з генераторами однієї конфігурації. На Visual Studio CMAKE_BUILD_TYPE порожня, і жодна гілка не спрацює. Вирази-генератори від цього застраховані, тому для коду, який має працювати всюди, надавайте їм перевагу.
Макроси в коді C++
Макроси, визначені в CMakeLists.txt, доступні в коді:
#include <iostream>
int main() {
std::cout << "Моя програма v1.0" << std::endl;
#ifdef DEBUG_MODE
std::cout << "[DEBUG] Запущено в режимі відладки" << std::endl;
#else
std::cout << "Release version" << std::endl;
#endif
#ifdef NDEBUG
std::cout << "NDEBUG визначено: assert вимкнено" << std::endl;
#else
std::cout << "NDEBUG не визначено: assert активний" << std::endl;
#endif
return 0;
}
Ту саму програму зібрано в двох конфігураціях:
Окремі папки для різних конфігурацій
Для single-config генераторів одна папка збірки відповідає одній конфігурації. Тому для двох конфігурацій потрібні дві папки:
# Debug збірка
cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build-debug
# Release збірка
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release
Вони не заважають одна одній, і не потрібно перебудовувати проєкт при кожному перемиканні. Це зручно, коли пишете код у Debug, а час від часу перевіряєте швидкодію у Release.
Multi-config генератори
Visual Studio та Xcode створюють один проєкт, у якому всі конфігурації доступні одночасно. Тип обирається при збірці:
# Одна конфігурація проєкту, усі типи доступні
cmake -S . -B build -G "Visual Studio 17 2022"
cmake --build build --config Debug
cmake --build build --config Release
| Генератор | Конфігурація задається | Виконуваний файл |
|---|---|---|
| Unix Makefiles, Ninja | -DCMAKE_BUILD_TYPE=... при конфігурації | build/MyApp |
| Visual Studio, Xcode | --config ... при збірці | build/Debug/MyApp.exe, build/Release/MyApp.exe |
| Ninja Multi-Config | --config ... при збірці | build/Debug/MyApp, build/Release/MyApp |
Саме через це в лекції про перший проєкт на Windows виконуваний файл лежав у підпапці Debug.
Змінні для прапорців конфігурацій
Прапорці кожної конфігурації зберігаються в змінних CMAKE_CXX_FLAGS_<КОНФІГУРАЦІЯ>:
# Додаткові прапорці для кожної конфігурації
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")
# Виведення поточних значень
message(STATUS "Compile flags: ${CMAKE_CXX_FLAGS}")
message(STATUS "Debug flags: ${CMAKE_CXX_FLAGS_DEBUG}")
message(STATUS "Release flags: ${CMAKE_CXX_FLAGS_RELEASE}")
CMakeLists.txt замінює значення за замовчуванням і ламає перенесення проєкту на MSVC (прапорці -g там не діють). Для власних доповнень краще використовувати target_compile_options з виразами-генераторами, залишаючи стандартні значення на совісті CMake.Різні бібліотеки для Debug і Release
Деякі зовнішні бібліотеки постачаються окремо для Debug та Release (наприклад, з суфіксом d). Вираз-генератор вибирає потрібну автоматично:
add_executable(MyApp main.cpp)
# Через if (тільки single-config)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_link_libraries(MyApp debug_lib)
else()
target_link_libraries(MyApp release_lib)
endif()
# Через вирази-генератори (працює скрізь)
target_link_libraries(MyApp
$<$<CONFIG:Debug>:debug_lib>
$<$<CONFIG:Release>:release_lib>
)
Існує і старий синтаксис з ключовими словами debug та optimized у target_link_libraries, але вирази-генератори надійніші. Це одна з причин, чому пакетні менеджери (vcpkg) самі підбирають правильну версію бібліотеки для кожної конфігурації.
Питання для самоперевірки
-DNDEBUG, а стандартний assert при визначеному NDEBUG розгортається в порожній вираз. Перевірок у машинному коді немає, тож тест нічого не перевіряє. Для тестів використовуйте Debug або фреймворк із власними макросами перевірок.-g, тобто відладочна інформація. У порожньому типі не додається жоден прапорець конфігурації: компілятор працює зі своїми налаштуваннями за замовчуванням. Для відладки потрібен саме Debug.if(CMAKE_BUILD_TYPE STREQUAL "Debug") не виконується, тому що Visual Studio є multi-config генератором: конфігурацію обирають при збірці, а на етапі конфігурації CMAKE_BUILD_TYPE порожня. Використовуйте вираз-генератор $<CONFIG:Debug>, який обчислюється для кожної конфігурації окремо.Практичне завдання
- Візьміть програму з прикладу вище.
- Зберіть її в
build-debug(Debug) таbuild-release(Release). - Запустіть обидві та порівняйте вивід.
- Додайте у
main.cppвикликassert(false && "перевірка")і перевірте, що відбувається в кожній збірці. - Додайте до CMake-проєкту виведення
CMAKE_CXX_FLAGS_DEBUGтаCMAKE_CXX_FLAGS_RELEASEза допомогоюmessage(STATUS ...).
assert(false ...) з повідомленням про рядок і файл. У Release assert зникне, програма відпрацює без помилки. Різниця демонструє, що перевірки на assert слугують допоміжним засобом налагодження, а не обробкою помилок: для помилок, які мають спрацьовувати завжди, використовуйте винятки або явні перевірки.Резюме
🧭 Чотири типи
CMAKE_BUILD_TYPE або параметром --config.🚩 Реальні прапорці
-g, Release -O3 -DNDEBUG. Саме NDEBUG вимикає assert() у всіх, крім Debug, конфігураціях.🧮 Вирази-генератори
$<$<CONFIG:Debug>:...> обчислюється для кожної конфігурації й працює з усіма генераторами.🗂️ Single і multi-config
CMakeLists.txt, власні модулі .cmake, попередньо скомпільовані заголовки та команду install.Умови, опції, цикли та функції в CMake
Умовні конструкції if/elseif/else, перевірка змінних і файлів, логічні оператори, платформо-залежні налаштування, опції option, цикли foreach і while, функції та макроси, генерація заголовка з версією через configure_file.
Структура великого проєкту: підпапки, модулі CMake, PCH та install
Рекомендована структура папок C++ проєкту, ієрархія CMakeLists.txt, внутрішні бібліотеки, власні модулі .cmake для попереджень компілятора, попередньо скомпільовані заголовки, команда install.