JavaRush /Курсы /C++ SELF /Target‑подход в CMake: свойства на target

Target‑подход в CMake: свойства на target

C++ SELF
31 уровень , 2 лекция
Открыта

1. От глобальных флагов к target‑подходу

Если вы раньше не работали с реальными проектами, то «глобальные флаги» звучат как что-то приятное: один раз написал set(...) — и всё вокруг стало лучше, красивее и быстрее. На практике это чаще похоже на попытку одним вентилем управлять всей системой отопления многоквартирного дома, где каждый сосед любит свою температуру. Итог обычно один: кому-то жарко, кому-то холодно, а виноват (конечно же) CMake.

С CMake у новичков часто появляется соблазн писать так:

# ПЛОХО (для нашей дисциплины target‑подхода)
set(CMAKE_CXX_STANDARD 23)
add_definitions(-DDEBUG)
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
include_directories(include)

Выглядит компактно. Но проблема в том, что такие настройки «размазываются» по проекту и со временем начинают вести себя как невидимая магия: вы уже не уверены, кто именно получил флаг -Wall, почему какой-то файл вдруг увидел заголовок, и почему один target собирается, а другой — ломается.

Чтобы почувствовать боль, достаточно представить, что в проекте появились:

  • два приложения (например, app и tool);
  • библиотека (например, core);
  • тесты (появятся позже в курсе, но мысль полезная уже сейчас).

И внезапно выясняется: приложению хочется одних предупреждений и макросов, библиотеке — других, а тесты вообще хотят жить своей жизнью.

В target‑подходе мы лечим это радикально: каждую настройку приклеиваем к конкретной цели. Тогда CMake‑файл начинает читаться не как «магический набор глобальных заклинаний», а как нормальное описание: вот цель, вот её исходники, вот её требования и правила сборки.

Что такое target в CMake

Слово «target» в CMake поначалу воспринимается как «имя будущего бинарника». Это частично правда, но слишком упрощённая. Гораздо полезнее думать так: target — это объект со свойствами. У него есть имя, список исходников, а ещё целая «папка» настроек: стандарт языка, макросы препроцессора, опции компиляции и позже — include‑пути и зависимости линковки.

В этой лекции зафиксируем главное правило:

В стиле target‑подхода мы стараемся не писать «глобальные флаги для всего проекта».
Мы пишем «свойства для конкретного target».

То есть вместо «где-то сверху задать всё для всех» мы делаем «вот target app, и рядом с ним аккуратно перечислены его требования».

На уровне синтаксиса это выражается в семействе команд target_*, например:

  • target_compile_features(...) — требования к возможностям компилятора (включая стандарт языка),
  • target_compile_definitions(...) — макросы препроцессора,
  • target_compile_options(...) — опции компилятора.

Сегодня сфокусируемся именно на этих трёх: они лучше всего показывают смысл target‑подхода и при этом не требуют ещё обсуждать заголовочные пути и линковку.

2. Мини‑проект MiniCalc

Структура проекта

Чтобы не говорить абстрактно, будем развивать маленькое консольное приложение. Пусть оно называется MiniCalc и умеет складывать числа через отдельную функцию. Приложение простое, но оно идеально подходит, чтобы потрогать настройки сборки: стандарт языка, макросы и предупреждения.

Структура на данный момент может быть такой:

MiniCalc/
  CMakeLists.txt
  src/
    main.cpp
    sum.cpp
    sum.hpp

Обратите внимание: мы пока держим sum.hpp рядом с .cpp в папке src/, чтобы не упираться в тему include‑директорий раньше времени. Переезд заголовков в include/ — это отдельная тема следующей лекции.

Исходники

Код будет максимально скромный.

src/sum.hpp:

#pragma once

int sum(int a, int b);

src/sum.cpp:

#include "sum.hpp"

int sum(int a, int b) {
    return a + b;
}

src/main.cpp:

#include <iostream>
#include "sum.hpp"

int main() {
    std::cout << "2 + 3 = " << sum(2, 3) << '\n'; // 2 + 3 = 5
    return 0;
}

Сборку начнём с минимального CMakeLists.txt, а потом будем его улучшать строго в target‑стиле.

3. Настройки цели через target_*

Стандарт языка через target_compile_features

Когда проект маленький, кажется, что стандарт языка можно «один раз выставить сверху» и забыть. Но это именно тот случай, когда удобство сегодня превращается в загадку завтра. Target‑подход предлагает простой принцип: стандарт — это требование конкретной цели, потому что именно цель компилируется, а не «проект вообще».

Минимальный CMakeLists.txt в target‑стиле может выглядеть так:

cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)

add_executable(app
    src/main.cpp
    src/sum.cpp
)

target_compile_features(app PRIVATE cxx_std_23)

Здесь важно сразу проговорить смысл строк без «магии».

add_executable(app ...) создаёт target app. Это наш будущий исполняемый файл.

target_compile_features(app PRIVATE cxx_std_23) говорит: «Чтобы собрать app, компилятор должен уметь C++23 (и мы хотим, чтобы сборка шла в этом стандарте)».

Ключевое слово PRIVATE пока воспринимайте как «это нужно только самому target’у app». Тонкости PUBLIC/INTERFACE особенно ярко раскрываются на include‑директориях и зависимостях, но мы уже сейчас привыкаем к мысли: у требований есть видимость, а не просто «вкл/выкл».

Почему это лучше, чем set(CMAKE_CXX_STANDARD 23)? Потому что так вы явно видите, какая цель требует C++23. Если в проекте появится вторая цель (другое приложение), вы сможете для неё задать другое требование — осознанно, а не случайно.

Макросы сборки через target_compile_definitions

Макросы препроцессора — штука коварная. Они похожи на «переключатели режима работы программы», но на самом деле это переключатели режима компиляции, то есть они меняют то, какой код вообще попадёт в итоговый бинарник. Поэтому хранить такие переключатели «глобально для всех» — почти всегда плохая идея: вы слишком легко делаете разные части проекта несовместимыми друг с другом.

В target‑подходе макросы задаются так:

target_compile_definitions(app PRIVATE APP_NAME="MiniCalc")
target_compile_definitions(app PRIVATE APP_VERSION=1)

Давайте встроим это в наш CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)

add_executable(app
    src/main.cpp
    src/sum.cpp
)

target_compile_features(app PRIVATE cxx_std_23)
target_compile_definitions(app PRIVATE APP_NAME="MiniCalc")
target_compile_definitions(app PRIVATE APP_VERSION=1)

Теперь используем эти макросы в main.cpp. Важно: если макрос не определён, компиляция может сломаться. Поэтому сделаем «план Б» — дефолтные значения через #ifndef.

src/main.cpp:

#include <iostream>
#include "sum.hpp"

#ifndef APP_NAME
#define APP_NAME "(unknown)"
#endif

#ifndef APP_VERSION
#define APP_VERSION 0
#endif

int main() {
    std::cout << APP_NAME << " v" << APP_VERSION << '\n'; // MiniCalc v1
    std::cout << "2 + 3 = " << sum(2, 3) << '\n';         // 2 + 3 = 5
    return 0;
}

Теперь у нас появляется хороший «мостик» между сборкой и кодом: CMake задаёт «параметры сборки» через target_compile_definitions, а C++ код аккуратно реагирует.

И здесь важная дисциплина: не превращайте макросы в ввод пользователя. Макросы — не замена std::cin. Они нужны для конфигурации сборки (например, включить отладочный лог, выбрать режим, зашить имя продукта), а не для «ввести число и посчитать».

Опции компилятора через target_compile_options

Предупреждения компилятора — это как замечания преподавателя на полях: игнорировать можно, но потом на экзамене будет больно. В реальных проектах предупреждения включают почти всегда, но включают осознанно и аккуратно. И тут target‑подход снова выигрывает: предупреждения — это свойство конкретной цели, а не абстрактной «сборки вообще».

Самый прямой вариант:

target_compile_options(app PRIVATE -Wall -Wextra)

Но есть нюанс: флаги зависят от компилятора. GCC/Clang понимают -Wall, MSVC — нет (у MSVC свои /W4 и друзья). Мы не уходим в глубокую кроссплатформенность, но покажем минимально адекватный вариант:

target_compile_options(app PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra>
)

Это выглядит страшнее, чем есть на самом деле. Сейчас важно понять не синтаксис «угловых скобок», а идею: опции приклеены к target’у app.

Если вы хотите совсем новичковый вариант без генераторных выражений (и готовы принять, что на MSVC будет иначе), можно начать так:

if (NOT MSVC)
    target_compile_options(app PRIVATE -Wall -Wextra)
endif()

Итого наш CMakeLists.txt становится таким:

cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)

add_executable(app
    src/main.cpp
    src/sum.cpp
)

target_compile_features(app PRIVATE cxx_std_23)
target_compile_definitions(app PRIVATE APP_NAME="MiniCalc" APP_VERSION=1)

if (NOT MSVC)
    target_compile_options(app PRIVATE -Wall -Wextra)
endif()

Заметьте характерный стиль target‑подхода: вы читаете файл сверху вниз и видите, что app — это не просто «бинарник», а цель с чётким набором требований.

Куда приклеиваются настройки в target‑подходе

Когда вы впервые слышите «свойства цели», может быть ощущение, что это всё те же флаги, только написанные другими словами. Чтобы это не осталось магией, полезно нарисовать простую схему: target — это центр, а команды target_* «навешивают» на него свойства. Так вы буквально видите, что происходит, и перестаёте воспринимать CMake как шаманство.

Вот упрощённая диаграмма (без заголовков и линковки — это отдельные лекции):

flowchart TD
    T["target: app (executable)"]

    S["sources: main.cpp, sum.cpp"] --> T
    F["compile features: cxx_std_23"] --> T
    D["compile definitions: APP_NAME, APP_VERSION"] --> T
    O["compile options: -Wall, -Wextra"] --> T

И вот здесь главный «психологический» эффект target‑подхода: настройки перестают быть «где-то в воздухе», они становятся частью описания конкретной цели.

Если позже у вас появится второй target (например, tool), вы не будете думать «а какие там глобальные флаги сейчас активны?». Вы просто откроете CMakeLists.txt и увидите: tool — вот такой, app — вот такой.

4. Типичные ошибки

Ошибка №1: продолжать жить в стиле CMAKE_CXX_FLAGS и «глобальной магии».
Очень легко начать красиво (создать add_executable, написать пару target_*), а потом «быстро накинуть» set(CMAKE_CXX_FLAGS "..."), потому что так где-то в интернете. Обычно это приводит к тому, что вы сами перестаёте понимать, почему один target собирается с предупреждениями, а другой внезапно — нет, или почему новые флаги ломают сборку на другой машине.

Ошибка №2: путать target_compile_features, target_compile_definitions и target_compile_options.
Новички иногда воспринимают это как «три способа задать одно и то же». На самом деле они про разное: features — про требования к языку/компилятору, definitions — про макросы препроцессора, options — про флаги компилятора. Если смешивать это, CMakeLists.txt превращается в нечитабельный «мешок флагов».

Ошибка №3: использовать макросы сборки как замену ввода данных.
Иногда хочется сделать -DINPUT=5 и считать, что это «параметр программы». Это параметр компиляции, а не параметр запуска. Вы встраиваете число в бинарник, а не читаете его из консоли. Такое решение может быть оправдано для версии/названия/режима логирования, но плохо подходит для «данных пользователя».

Ошибка №4: включить опции компилятора «для всех и навсегда» без понимания цены.
Даже полезные флаги могут быть проблемой, если их включить глобально: где-то они вызовут предупреждение, которое в одном target допустимо, а в другом — ломает сборку. Target‑подход как раз и учит включать качество сборки дозированно: конкретной цели — конкретные правила.

Ошибка №5: стесняться длинного CMakeLists.txt и пытаться любой ценой сократить строки.
Новичку кажется, что хороший CMakeLists.txt обязан быть коротким. На практике хороший CMakeLists.txt обязан быть понятным. Если понятность требует нескольких дополнительных строк рядом с add_executable(app ...) — это нормально. Страшно не то, что «много строк», а то, что «настройки спрятаны неизвестно где».

1
Задача
C++ SELF, 31 уровень, 2 лекция
Недоступна
Контракт стандарта
Контракт стандарта
1
Задача
C++ SELF, 31 уровень, 2 лекция
Недоступна
Витрина версии
Витрина версии
1
Задача
C++ SELF, 31 уровень, 2 лекция
Недоступна
Две роли
Две роли
1
Задача
C++ SELF, 31 уровень, 2 лекция
Недоступна
Строгая сборка
Строгая сборка
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ