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) (формулювання залежить від компілятора й платформи, але сенс один).
Це хороший діагностичний маркер:
- «не знайдено заголовок» — найчастіше проблема в шляхах до заголовків або в розташуванні самого заголовка;
- undefined reference — найчастіше це проблема лінкування: потрібний .cpp не бере участі у збиранні або не підʼєднано потрібну бібліотеку.
У нашому мінімальному світі причина зазвичай одна: ви забули файл у списку add_executable.
4. Схема процесу збирання
Щоб не потонути в командах, зафіксуємо процес у вигляді простої схеми. Це не «офіційна діаграма CMake», а зручна модель для розуміння.
flowchart TD
A["CMakeLists.txt (опис)"] --> B["Конфігурація CMake (підготовка збирання)"]
B --> C["Система збирання (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. Корисна звичка: спочатку визначити рівень помилки, а вже потім виправляти її в правильному місці.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ