1. Минимальный CMake‑проект
Когда вы только начинаете писать на C++, кажется, что мир прост: один main.cpp, одна кнопка Run — и всё работает. Но как только появляется второй .cpp, а потом третий, сборка внезапно превращается в квест «найди, где потерялся sum.cpp». В этот момент закономерно возникает вопрос: можно ли описать проект так, чтобы сборка была не магией, а рецептом?
CMake в минимальном виде — это и есть рецепт. Он не «компилирует вместо компилятора», он говорит: «вот мой проект, вот исходники, собери мне приложение с таким-то именем и используй такой-то стандарт языка». Даже такой минимум убирает огромный пласт хаоса: всё важное становится записанным, повторяемым и одинаковым для всех.
Чтобы дальше не возникало ощущения «CMake — это религия с заклинаниями», держим в голове одну простую мысль: минимальный CMakeLists.txt должен отвечать на три вопроса — как зовут проект, что мы собираем (какую цель), и на каком стандарте C++ живём.
Важно психологически разделять два мира:
- main.cpp — это то, что будет выполняться процессором.
- CMakeLists.txt — это то, что будет «выполняться» CMake’ом на этапе конфигурации сборки.
Если перепутать эти уровни, получается классика: «я починил main.cpp, а ошибка не ушла» — потому что ошибка вообще-то в описании сборки.
CMakeLists.txt обычно лежит в корне проекта. Он читается сверху вниз, но в целом стиль там декларативный: вы перечисляете цели сборки и из чего они состоят. В минимальном варианте нам не нужно знать «всё про CMake», нам нужна скелетная структура, которая стабильно работает и не мешает проекту расти.
2. Минимальный CMakeLists.txt: ключевые команды
В минимальном проекте нам достаточно трёх командных «кирпичиков»: project(...), add_executable(...) и фиксации стандарта (например, через target_compile_features(...)).
project(...): имя проекта и включение C++
Команда project(...) объявляет проект: даёт ему имя и говорит CMake, какими языками мы пользуемся. Для нас сейчас важнее всего включить C++ как язык сборки, чтобы CMake корректно применял C++‑настройки.
Простейший (и вполне нормальный) вариант:
project(MiniApp LANGUAGES CXX)
Здесь MiniApp — имя проекта. Оно не обязано совпадать с именем исполняемого файла, который мы будем собирать дальше (хотя часто совпадает «для спокойствия»). LANGUAGES CXX — явная просьба: «в этом проекте мы используем C++». Если не указать языки, CMake обычно догадается, но мы сейчас тренируем привычку писать «не как повезёт», а «как договорились».
Практический нюанс: имя проекта вы будете видеть в IDE, в логах сборки, иногда в путях сборочных папок. Поэтому лучше не называть проект Test или NewProjectFinalFinal2. В мире программирования слово “final” работает как магнит: как только вы его написали, оно тут же перестаёт быть финальным.
add_executable(...): цель приложения и список .cpp
Если мы хотим собрать приложение (исполняемый файл), в CMake это делается через add_executable. И здесь важно понимать смысл: мы создаём цель сборки (target), у которой есть имя и список исходников.
Минимальная форма:
add_executable(app src/main.cpp)
Читается почти как русское предложение: «добавь исполняемый файл app, собирается из src/main.cpp».
Ключевой практический момент: CMake не «угадывает» ваши .cpp файлы магически (в базовой дисциплине). Если вы добавили новый src/sum.cpp, но не добавили его в add_executable, компилятор может честно собрать main.cpp, а линкер потом честно скажет: «а где определение функции?».
То есть add_executable — это не просто «создать бинарник». Это ещё и список того, что реально участвует в сборке. Если файл не указан — значит он не собирается. Иногда новички воспринимают это как «CMake вредничает», но на самом деле это способ делать сборку прозрачной и воспроизводимой.
Пример с несколькими .cpp:
add_executable(app
src/main.cpp
src/sum.cpp
)
Переносы строк и скобки — просто удобство чтения. CMake воспринимает это как один список.
Фиксация C++23: зачем и как
Стандарт языка нужно фиксировать явно. Иначе один компилятор по умолчанию соберёт как C++17, другой как C++20, третий как C++14, и вы получите сборку уровня «лотерея, но без выигрыша».
В target‑подходе это обычно делают так:
target_compile_features(app PRIVATE cxx_std_23)
Смысл: «для цели app требуем возможности компилятора на уровне C++23». Слово PRIVATE пока можно воспринимать так: «это требование относится к сборке самого app».
Есть и более старый стиль через глобальную переменную CMAKE_CXX_STANDARD, который вы встретите в примерах:
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Он тоже работает, и в маленьких проектах его иногда используют. Но когда целей становится больше одной (например, приложение и библиотека), target‑подход обычно дисциплинированнее: требования не «размазаны» по проекту, а лежат рядом с конкретной целью.
Практическое правило на сейчас: фиксируйте стандарт так, чтобы это было видно рядом с add_executable. Тогда даже через полгода вы не будете гадать, что тут предполагалось.
3. Практический пример: приложение «Сумма»
Чтобы CMake не оставался абстракцией, соберём маленький, но честный многофайловый пример. Пусть это будет консольное приложение, которое считает сумму двух целых чисел через отдельную функцию.
Структура файлов:
MiniApp/
CMakeLists.txt
src/
main.cpp
sum.hpp
sum.cpp
Заголовок с объявлением функции:
// src/sum.hpp
#pragma once
int sum(int a, int b);
Реализация в .cpp:
// src/sum.cpp
#include "sum.hpp"
int sum(int a, int b) {
return a + b;
}
main.cpp, который вызывает функцию:
// src/main.cpp
#include <iostream>
#include "sum.hpp"
int main() {
int a = 2;
int b = 3;
std::cout << "sum = " << sum(a, b) << '\n'; // sum = 5
}
Здесь полезно удержать простую «трёхступенчатую» логику:
- Компилятор компилирует main.cpp, где видит объявление sum из sum.hpp.
- Компилятор компилирует sum.cpp, где лежит определение sum.
- Линкер склеивает объектники в один исполняемый файл.
Если в CMakeLists.txt забыть sum.cpp, компиляция main.cpp пройдёт, а линковка — нет. И это не «каприз», а честная математика: объявление есть, определения нет.
Полный минимальный CMakeLists.txt для примера
Нам нужно: объявить проект на C++, создать приложение app, перечислить .cpp, зафиксировать C++23.
Вариант, который хорошо читается и обычно «просто работает»:
cmake_minimum_required(VERSION 3.20)
project(MiniApp LANGUAGES CXX)
add_executable(app
src/main.cpp
src/sum.cpp
)
target_compile_features(app PRIVATE cxx_std_23)
Несколько пояснений, чтобы файл не выглядел магическим:
- cmake_minimum_required(...) фиксирует минимальную версию CMake, на которую вы рассчитываете. Это похоже на «минимальную версию ОС» в мобильной разработке: вы не хотите случайно использовать возможности нового CMake и потом удивляться, что на другом компьютере сборка не запускается.
- В add_executable(...) мы перечисляем .cpp (то, что реально компилируется как единицы трансляции). Заголовки (.hpp) обычно не перечисляют, потому что их «подтягивает» #include на уровне C++ кода. Иногда заголовки добавляют в IDE для навигации, но это не заменяет компилируемые файлы.
Что будет, если забыть sum.cpp в add_executable
Представьте, что вы написали sum.cpp, но в CMakeLists.txt оставили только main.cpp:
add_executable(app
src/main.cpp
)
Что произойдёт:
- main.cpp скомпилируется, потому что компилятор видит #include "sum.hpp" и объявление int sum(int, int);
- на этапе линковки вы получите ошибку вида undefined reference to sum(int, int) (формулировка зависит от компилятора и платформы, но смысл один).
Это хороший диагностический маркер:
- «не найден header» — чаще проблема include‑путей или расположения заголовка;
- undefined reference — чаще проблема линковки: не участвует нужный .cpp или не подключена нужная библиотека.
В нашем минимальном мире причина обычно одна: забыли файл в списке add_executable.
4. Схема процесса сборки
Чтобы не утонуть в командах, зафиксируем процесс в виде простой схемы. Это не «официальная диаграмма CMake», а удобная модель для головы.
flowchart TD
A["CMakeLists.txt (описание)"] --> B["CMake configure (подготовка сборки)"]
B --> C["Build system (IDE или генератор)"]
C --> D["Компилятор компилирует src/main.cpp -> main.o"]
C --> E["Компилятор компилирует src/sum.cpp -> sum.o"]
D --> F["Линкер собирает app из main.o и sum.o"]
E --> F
Смысл схемы в том, что есть «слой описания» и «слой реального компилирования». CMake читает CMakeLists.txt и готовит правила сборки, но не заменяет компилятор и линкер.
Поэтому, когда вы видите ошибку, полезно спрашивать себя: «она на уровне CMake (описание), на уровне компиляции (C++ синтаксис/типы) или на уровне линковки (не хватает определений)?». Эта привычка экономит часы — а часы жизни не подключаются через #include.
5. Типичные ошибки
Ошибка №1: путать имя проекта и имя исполняемого файла.
Новички иногда думают, что project(MiniApp) автоматически создаёт бинарник MiniApp. На самом деле project(...) — это имя проекта как сущности для CMake/IDE, а бинарник задаётся в add_executable(app ...). Можно сделать их одинаковыми, но это не одно и то же, и CMake не обязан «угадывать ваше настроение».
Ошибка №2: забыть добавить новый .cpp в add_executable.
Самая частая причина линковочных ошибок в маленьких проектах. Вы подключили заголовок в main.cpp, компиляция прошла, а линковка упала с undefined reference. В этот момент не нужно добавлять ещё больше #include — это обычно только ухудшает ситуацию. Нужно проверить: участвует ли .cpp с определением в цели сборки.
Ошибка №3: пытаться «добавить заголовок в сборку», чтобы починить линковку.
Иногда встречается мысль: «раз не хватает sum, добавлю sum.hpp в add_executable». Заголовок не является единицей трансляции и не даёт определений сам по себе. Он полезен как часть структуры проекта, но он не заменяет .cpp. Линковка чинится либо добавлением .cpp, либо подключением библиотеки (но это уже другой сценарий).
Ошибка №4: не фиксировать стандарт языка и зависеть от «дефолта».
Сегодня IDE могла собрать проект как C++23 (потому что так настроено окружение), а завтра на другой машине это будет C++17. Результат — странные ошибки «у меня компилируется, у друга нет», и начинаются мифы про «разные ауры компьютеров». Фиксация стандарта через target_compile_features(... cxx_std_23) возвращает всё в инженерную реальность.
Ошибка №5: пытаться лечить ошибки CMake правками C++ кода (и наоборот).
Если CMake ругается, что не знает команду или не может найти файл, это не проблема main.cpp. И наоборот: если компилятор ругается на синтаксис или типы, это не чинится добавлением команд в CMakeLists.txt. Полезная привычка: сначала определить уровень ошибки, и только потом чинить в правильном месте.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ