1. Збирання стає окремим завданням
Коли ви пишете перші програми, здається, що збирання — це магія кнопки Run. Натиснули — і «воно працює». Але щойно проєкт виростає бодай трохи й перестає вміщатися в один файл, ця магія починає пахнути… трохи підгорілим пластиком. Зʼявляються заголовки, кілька .cpp, папки include/ і src/, і раптом ви витрачаєте час уже не на логіку програми, а на запитання: «Чому це не збирається на моєму ноутбуці, а в друга — збирається?»
Скажімо чесно: C++ не «ламається» — ламається наша наївна модель «проєкт = один файл». Насправді проєкт — це набір вихідних файлів, залежностей і правил збирання. І ці правила десь мають бути зафіксовані.
Уявіть, що ви печете хліб. Код — це інгредієнти. Компілятор — духовка. Але без рецепта — скільки борошна, скільки води, яка температура і скільки часу — результат дуже залежатиме і від настрою пекаря, і від умов довкола. CMake — саме такий рецепт: його можна прочитати і на кухні Windows, і на кухні Linux, і на кухні macOS.
У цей момент інколи виникає спокуса: «Гаразд, я просто запускатиму компілятор якось… вручну». Для одного файла це цілком реально. Але в багатофайловому проєкті дуже швидко стає виснажливо: доводиться памʼятати, які .cpp входять до програми, які include-шляхи потрібні, які прапорці використовувати і в якому порядку все це звʼязувати.
Якщо не заглиблюватися в командний рядок і конкретні ключі, збирання концептуально виглядає так:
main.cpp -> main.o
task.cpp -> task.o
io.cpp -> io.o
(main.o + task.o + io.o) -> app.exe
Проблема не лише в тому, що це складно. Така схема ще й дуже крихка: додали parser.cpp, але забули включити його до збирання, — отримали «undefined reference»; перейменували папку include/, десь не оновили шляхи, — отримали «file not found»; змінили IDE — і все зламалося, бо налаштування були заховані в галочках.
CMake потрібен, щоб правила були не у вас у голові й не в «священних налаштуваннях IDE», а в репозиторії поруч із кодом.
2. Система збирання: компілятор, лінкер і «диригент»
Якщо компілятор і лінкер — це «музиканти», то системі збирання потрібен диригент: хтось має визначити, хто і коли грає, які партії потрібні та що робити, якщо в проєкті зʼявився ще один «скрипаль» — новий .cpp.
Тут важливо чітко розрізняти ці поняття.
Компілятор (наприклад, GCC/Clang/MSVC) уміє компілювати код C++. Лінкер — поєднувати обʼєктні файли й бібліотеки. Але жоден із них не зобовʼязаний розуміти структуру проєкту як цілісність: де лежать вихідні файли, які є залежності, які include-шляхи потрібні, які прапорці використовувати та які бібліотеки підʼєднувати.
Щоб усе це можна було відтворити, потрібен опис збирання.
Невелика схема, щоб зафіксувати ідею:
flowchart TD
A["Ваш код: .cpp/.hpp"] --> B["Опис збирання: правила проєкту"]
B --> C["Генератор збирання / проєкт IDE"]
C --> D["Компілятор компілює .cpp"]
D --> E["Лінкер звʼязує обʼєктні файли"]
E --> F["Готовий виконуваний файл"]
Тут CMake займає місце «опису збирання» і частково «генератора»: він читає ваш рецепт і генерує те, з чим уміє працювати конкретне середовище.
Коли щось не збирається, новачкові часто хочеться узагальнити: «Компʼютер мене ненавидить». Але збирання — це конвеєр. Якщо він ламається, важливо зрозуміти, на якій саме ділянці.
Проблема в описі збирання
Це ситуація, коли правила збирання неправильні або неповні. Наприклад: «Ми забули сказати, що існує src/task.cpp». Компілятор може навіть не побачити цей файл, бо його просто не включили до збирання.
Помилка компіляції
Це ситуація, коли компілятор обробляє .cpp і не може його скомпілювати. Типові причини: синтаксична помилка, не знайдено заголовок, не збігаються типи або забуто ;.
Класичний приклад: include-шлях не налаштовано, і компілятор не знаходить наш заголовок:
#include "todo/task.hpp" // помилка: файл не знайдено
Помилка лінкування
Це ситуація, коли кожен .cpp окремо скомпілювався, але лінкер не зміг знайти визначення. Те саме «undefined reference», через яке новачкам іноді хочеться змінити професію й піти вирощувати полуницю. Хоча полуниця теж уміє вередувати.
Наприклад, якщо print_task оголосили, але її реалізацію не додали до збирання, лінкування завершиться помилкою.
І саме тут CMake стає важливим: він допомагає зробити так, щоб проєкт збирався однаково, а список файлів і правил існував не лише у вас у голові.
3. Кросплатформне збирання та переваги CMake
Слово «кросплатформний» інколи звучить як маркетинговий штамп, але для C++ це майже побутова необхідність. Люди працюють у різних операційних системах і з різними інструментами.
На Windows часто використовують Visual Studio і компілятор MSVC; на Linux — GCC/Clang і збирання через Make/Ninja; на macOS — Xcode і Clang. Навіть якщо код однаковий, спосіб його зібрати буде різним: інші формати проєктів, інші ключі, інші розширення, інші очікування.
Щоб відчути це на практиці, ось табличка — спрощена, але чесна за змістом:
| Що ви хочете отримати | Типовий інструмент | Що реально «розуміє» інструмент |
|---|---|---|
| Visual Studio Solution | Visual Studio / MSBuild | |
| Makefile‑збирання | |
|
| Ninja‑збирання | |
|
| Xcode‑проєкт | Xcode | |
І саме тут зʼявляється CMake як перекладач: ви один раз описуєте проєкт, а далі CMake вміє згенерувати потрібний формат для поточного середовища.
Ключова думка: CMake — не «ще один компілятор». Це інструмент, який робить збирання переносним і відтворюваним.
Важливо підсумувати це, не перетворюючи розмову на довідник команд: синтаксис буде далі. CMake дає три великі переваги, які відчутні навіть у невеликих проєктах.
Перше — єдине джерело правди про збирання. Проєкт описано в тексті: його можна читати, перевіряти під час ревʼю, змінювати й зберігати в Git. Це різко зменшує кількість «магічних збирань», які працюють лише на компʼютері автора.
Друге — кросплатформність. Ви описуєте проєкт один раз, а потім можете зібрати його в різних середовищах, бо CMake вміє «перекласти» цей опис у формат, який розуміє конкретна система збирання або IDE.
Третє — масштабованість. Коли проєкт зростає, ви додаєте нові частини — наприклад, бібліотеку з логікою, окремий застосунок, залежності та налаштування. Якщо від самого початку мислити проєктом як набором «цілей збирання» (targets), хаосу й болю буде значно менше.
4. CMake як опис проєкту на прикладі TodoApp
Зараз хочеться сказати: «Ну, CMake просто збирає». Але це хибна ментальна модель. Згодом саме вона заважає нормально налагоджувати проблеми зі збиранням.
Правильніше сказати так: CMake описує проєкт.
Тобто ви фіксуєте, з яких частин складається проєкт — програми, бібліотеки, — які вихідні файли до них входять, які include-шляхи їм потрібні, які макроси й прапорці компіляції застосовуються, які залежності потрібні під час лінкування.
І головне: цей опис має бути таким, щоб інша людина — або навіть ваш власний компʼютер за місяць — могла відтворити збирання без шаманства в дусі «постав галочку в IDE, потім ще одну, а потім перезапусти й помолися».
Щоб це не звучало надто абстрактно, привʼяжімо цю ідею до нашого навчального застосунку.
Уявімо, що ми поступово розвиваємо простий консольний застосунок: TODO‑список. Нічого «космічного»: додати завдання, вивести список. Ми беремо цей приклад не тому, що мріємо конкурувати з Notion, а тому, що такий проєкт природно стає багатофайловим.
Нехай структура проєкту така:
TodoApp/
include/
todo/
task.hpp
io.hpp
src/
main.cpp
task.cpp
io.cpp
У нас є модель Task і функції виведення.
Приклад заголовка моделі:
// include/todo/task.hpp
#pragma once
#include <string>
namespace todo {
struct Task {
int id = 0;
std::string text;
bool done = false;
};
} // namespace todo
І, наприклад, проста функція виведення завдання (реалізація — у .cpp):
// src/task.cpp
#include "todo/task.hpp"
#include <iostream>
namespace todo {
void print_task(const Task& t) {
std::cout << t.id << ": " << t.text << '\n'; // 1: Купити молоко
}
} // namespace todo
А main.cpp викликає це:
// src/main.cpp
#include "todo/task.hpp"
#include <vector>
int main() {
std::vector<todo::Task> tasks = { {1, "Купити молоко", false} };
// todo::print_task(tasks[0]); // (поки не підʼєднали оголошення)
}
Навіть на цьому етапі багато новачків уже вловлюють першу важливу думку: щоб викликати print_task, треба, по‑перше, мати оголошення в заголовку, а по‑друге — реалізацію в .cpp у складі збирання.
І саме на цьому кроці стає видно, чому «опис проєкту» такий важливий.
Target як «одиниця збирання»
Ми ще не заглиблюємося в команди CMake — це наступний крок. Але одну ідею дуже корисно зафіксувати вже сьогодні.
У CMake центральне поняття — target, тобто ціль збирання. По суті, це те, що ми хочемо отримати: застосунок або бібліотеку.
Target — це не просто назва майбутнього результату збирання. Це ще й набір властивостей, які визначають, з яких вихідних файлів збирати, де шукати заголовки, які вимоги до стандарту мови має проєкт, які залежності потрібні для лінкування тощо.
Якщо сказати простіше, target — це як «страва в меню», у якої є рецепт та інгредієнти. А CMake‑проєкт — це ціла кухня, де може бути кілька страв.
5. Типові помилки
Помилка № 1: думати, що CMake «компілює код».
Це дуже поширена й цілком зрозуміла помилка: в IDE ви натиснули кнопку — і все ніби сталося само собою. Але правильна картина така: CMake описує, як збирати проєкт, а компілятор і лінкер виконують реальну роботу. Якщо плутати ці рівні, ви почнете лагодити не те: наприклад, намагатиметеся виправити C++‑код, хоча насправді просто не додали .cpp до збирання.
Помилка № 2: сподіватися, що система збирання «сама знайде всі файли».
Новачки часто думають: «Ну, раз файл лежить у папці src/, він має збиратися». Ні. За замовчуванням будь-яка система збирання працює лише з тим, що їй явно вказали. Якщо новий .cpp не включено в правила збирання, він просто не братиме участі в процесі, а ви отримаєте помилки лінкування або здивовано питатимете себе: «Чому мій код не виконується?»
Помилка № 3: плутати «підʼєднав заголовок» і «додав реалізацію».
#include "todo/task.hpp" лише робить оголошення видимим для компілятора. Але лінкеру все одно потрібні визначення. Якщо реалізація функції лежить у task.cpp, цей файл має брати участь у збиранні — безпосередньо або через бібліотеку. Це різні рівні, і CMake допомагає тримати їх окремо, щоб не плутати.
Помилка № 4: намагатися лагодити збирання галочками в IDE, не фіксуючи правила в проєкті.
Так, інколи така «галочка» рятує просто зараз. Але за тиждень ви забудете, яка саме галочка була важливою, колега не зможе це повторити, і проєкт працюватиме «лише на одному компʼютері». Саме тому CMake цінний: правила збирання описані явно й придатні до перенесення.
Помилка № 5: звалювати все в одну купу, не думаючи про структуру.
Коли проєкт зростає, дуже хочеться «швидше зробити, аби збиралося», і тоді зʼявляються рішення на кшталт «додаймо в include-шляхи весь диск C:». На короткій дистанції це може зменшити кількість помилок «file not found», але в довгій перспективі перетворює проєкт на загадку. Ідея CMake як «опису проєкту» якраз про протилежне: збирання має бути прозорим і мінімально достатнім, а не магічним і надмірним.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ