JavaRush /Курсы /C++ SELF /Минимальный CMakeLists.txt: project

Минимальный CMakeLists.txt: project

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

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
}

Здесь полезно удержать простую «трёхступенчатую» логику:

  1. Компилятор компилирует main.cpp, где видит объявление sum из sum.hpp.
  2. Компилятор компилирует sum.cpp, где лежит определение sum.
  3. Линкер склеивает объектники в один исполняемый файл.

Если в 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. Полезная привычка: сначала определить уровень ошибки, и только потом чинить в правильном месте.

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