JavaRush /Курси /C++ SELF /Структура репозиторію: include/, src/, tests/ і build/

Структура репозиторію: include/, src/, tests/ і build/

C++ SELF
Рівень 26 , Лекція 4
Відкрита

1. Базова схема папок проєкту

Коли проєкт маленький, здається, що структура папок — суцільна естетика: мовляв, «ну лежить усе поруч — і гаразд». Але щойно файлів стає більше ніж три, мозок починає працювати, як компілятор без оптимізацій: повільно, нервово й підозріло. Хороша структура репозиторію потрібна не для краси. Вона потрібна, щоб ви швидко розуміли, де що шукати, і щоб проєкт не розвалився, коли ви додасте десятий модуль.

Уявіть, що ви відкрили чужий проєкт. Телепатії у вас немає, зате є файловий менеджер. Якщо структура адекватна, ви буквально за хвилину здогадаєтеся, де точка входу, де заголовки, де реалізація, а де перевірка. Якщо структури немає, ви починаєте шукати main() по всьому проєкту, як голку в сіні. Причому цю голку ще й перейменували на main_final_final2.cpp.

У навчальному курсі структура особливо важлива: ви поступово додаватимете функціональність до одного застосунку. Якщо папки й назви узгоджені, нові шматки коду природно стають на свої місця.

Зараз ми домовимося про дуже просту, але цілком життєздатну «географію» проєкту. Вона не єдина у світі, але для навчання й більшості невеликих застосунків підходить чудово. І головне: вона добре поєднується з нашою моделлю «модуль = .hpp + .cpp» та з ідеєю «інтерфейс окремо від реалізації».

Нижче — невелика таблиця-орієнтир. Її не потрібно завчати, як таблицю множення. Краще зрозуміти логіку: що підключають інші файли і що є результатом збирання.

Папка Що там лежить Як це читати по‑людськи
include/
Заголовки .hpp «Вітрина проєкту»: оголошення типів і функцій, які можна підключати з різних .cpp
src/
Вихідні файли .cpp (і зазвичай main.cpp) «Кухня проєкту»: реалізації функцій, логіка, деталі
tests/
Невеликі перевірочні програми (часто окремі main) «Перевіримо модуль окремо»: швидко запустити й переконатися, що все базово працює
build/
Артефакти збирання (те, що створює компілятор або IDE) «Майстерня»: потрібне для збирання, але не є вихідними файлами

Чому саме так? Бо такий підхід розділяє ролі: вихідні файли — окремо, інтерфейс — окремо, експерименти й перевірки — окремо, результати збирання — окремо. І так, це прибирає вічний хаос у стилі «а що з цього взагалі треба надсилати другові, викладачеві чи в репозиторій».

include/: заголовки й публічний інтерфейс

Коли ви кладете заголовки в include/, ви ніби кажете: «Ось цим можна користуватися». Це особливо зручно, коли у вас кілька модулів і вони підключають заголовки один одного. Тоді include/ стає єдиним джерелом істини, і проєкт менше залежить від випадкового порядку файлів або від того, «у якій папці я зараз відкрив IDE».

Важлива думка: заголовок — не місце, де ви «ховаєте побільше коду». Тут ви пояснюєте решті програми, які типи й функції існують та як ними користуватися. Ми вже обговорювали межу «інтерфейс/реалізація»: у заголовку зазвичай містяться struct, enum class та оголошення функцій, а реалізація — у .cpp.

Ще одна практична причина тримати заголовки в include/: коли у вас зʼявляться окремі перевірочні програми в tests/, вони підключатимуть той самий інтерфейс, що й основний застосунок. Це різко зменшує ризик того, що тести «перевіряють не те».

src/: реалізації та точка входу

Папка src/ — це місце, де міститься все, що реально виконується: тіла функцій, алгоритми, обробка введення та виведення, робота з контейнерами тощо. І майже завжди саме тут лежить main.cpp, бо main — це частина застосунку, тобто реалізації, а не інтерфейсу.

Якщо ви покладете main.cpp куди завгодно, проєкт фізично не зламається — компілятор не образиться. Але морально — так: ви щоразу заново згадуватимете, де вхід у програму. У нормальній структурі точка входу має бути очевидною: відкрили src/, знайшли main.cpp, зрозуміли, звідки стартуєте.

Практичне правило, яке корисно тримати в голові: у src/ можна підключати що завгодно, а в include/ краще бути стриманішими. Ми не заглиблюватимемося зараз у тонкощі того, що саме підключати, — це окрема тема далі в курсі. Але загальний принцип такий: заголовки мають бути максимально простими й стабільними, а реалізація може дозволити собі більше деталей.

tests/: швидкі перевірки модулів

Папка tests/ у нашому курсі — це не про складні тестові фреймворки й не про промисловий CI. Ідея значно простіша: іноді зручно перевірити модуль окремо від усього застосунку. Наприклад, ви написали функцію форматування або друку моделі й хочете переконатися, що вона працює, не запускаючи весь сценарій застосунку.

Зазвичай такі перевірки — це окремий файл з int main(), який підключає заголовок модуля й робить кілька викликів. По суті, це міні‑програма «для себе», але збережена поруч із проєктом. І це різко пришвидшує розробку: ви не витрачаєте час на введення даних, меню чи складний сценарій — просто запускаєте крихітний тестовий main і дивитеся на результат.

Найважливіша перевага папки tests/ в тому, що ви заздалегідь привчаєте проєкт до ідеї «модуль можна використовувати ззовні». А отже, ви майже автоматично починаєте писати акуратніші інтерфейси в заголовках.

build/: артефакти збирання і чому їх не комітять

Папка build/ — це місце, куди потрапляють результати збирання: тимчасові файли, обʼєктні файли, кеш збирання, інколи згенеровані файли проєкту і, зрештою, зібраний виконуваний файл. Вона зʼявляється тому, що збирання — окремий процес із купою технічних кроків.

Чому build/ не зберігають у репозиторії? Бо це не вихідні файли, а «продукти виробництва». По‑перше, вони часто залежать від вашої машини й налаштувань: інша ОС, інший компілятор, інший шлях до проєкту — і вміст уже буде іншим. По‑друге, вони можуть важити чимало. По‑третє, їх легко відтворити: вихідні файли важливіші.

Дуже корисна звичка — ставитися до build/ як до папки, яку не шкода видалити. Якщо вихідні файли на місці, ви будь‑коли зможете зібрати все заново. Це зменшує страх «я щось зламав», бо ви розумієте: ви не видаляєте код, а прибираєте результат збирання.

І ще один момент: build/ краще тримати окремою папкою, щоб вихідні файли та «виробниче сміття» не змішувалися. Тоді ви не побачите в src/ загадкових файлів із розширеннями, яких самі не створювали, і не почнете підозрювати компʼютер в одержимості.

Угода «одна сутність — одна пара файлів»

Коли ви вибудовуєте структуру include/ і src/, дуже зручно мислити так: кожен модуль — це пара файлів. Наприклад, є модуль task — отже, є task.hpp і task.cpp. Це не закон фізики, але хороша угода, яка допомагає тримати проєкт у порядку.

Усередині include/ ви тримаєте заголовок, а всередині src/ — реалізацію. Тоді проєкт читається майже як словник: «шукаю інтерфейс — іду в include/; шукаю, як це зроблено, — іду в src/». А ще це зменшує ризик того, що ви випадково почнете підключати .cpp замість .hpp (так, люди справді так роблять — зазвичай у паніці, і зазвичай це закінчується дивними помилками).

Ідея «одна сутність — одна пара» особливо корисна для новачків, бо прибирає зайвий вибір. Не потрібно щоразу вирішувати: «куди б мені покласти цю функцію?» Ви просто питаєте себе: «вона належить до модуля task?» Якщо так, то в task.hpp буде оголошення, а в task.cpp — реалізація.

2. Практичний приклад: міні‑проєкт Tasky

Зараз зберемо невеликий «скелет» проєкту, який далі можна буде розширювати в курсі. Застосунок буде простим: зберігати завдання (Task) і друкувати його. Наша мета — не крута функціональність, а розуміння того, як файли й папки працюють разом.

Дерево проєкту

Спочатку — структура. Її зручно тримати в голові як мапу:

tasky/
  include/
    app/
      task.hpp
      task_print.hpp
  src/
    task_print.cpp
    main.cpp
  tests/
    task_print_smoke.cpp
  build/
    (створюється збиранням, у репозиторій не кладемо)

Зверніть увагу на include/app/...: це не обовʼязково, але зручно. Так ви одразу бачите, що заголовки належать вашому застосунку, а не чужій бібліотеці, і назви менше конфліктують.

Модель даних: Task

include/app/task.hpp

#pragma once // (поки що просто як позначка; деталі розберемо іншим разом)

#include <string>

namespace app {

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

} // namespace app

Тут ми описали просту модель. Вона лежить у include/, бо Task потрібен «усім»: і застосунку, і тестам, і будь‑яким майбутнім модулям.

Якщо ви помітили #pragma once і думаєте: «А ми це не проходили», — ви маєте рацію. У межах цієї лекції можете сприймати це як технічну позначку, щоб файл випадково не підключали двічі. Детально гігієну заголовків ми розбиратимемо окремо, пізніше.

Інтерфейс друку: окремий заголовок

include/app/task_print.hpp

#pragma once

#include <iosfwd>
#include "app/task.hpp"

namespace app {

void print_task(std::ostream& out, const Task& t);

} // namespace app

Зверніть увагу: заголовок сам нічого не друкує. Він лише повідомляє: «Є функція print_task, ось її сигнатура». Це і є «вітрина».

Реалізація друку в src/

src/task_print.cpp

#include "app/task_print.hpp"

#include <ostream>

namespace app {

void print_task(std::ostream& out, const Task& t) {
    out << "#" << t.id << " [" << (t.done ? 'x' : ' ') << "] " << t.title;
}

} // namespace app

Ключова думка: реалізація живе в src/. Ззовні користувачам модуля не важливо, як саме ви друкуєте. Їм важливо знати, що функція існує і як її викликати.

Лаконічний main.cpp у src/

src/main.cpp

#include <iostream>
#include "app/task_print.hpp"

int main() {
    app::Task t{.id = 1, .title = "Здати домашнє завдання", .done = false};

    app::print_task(std::cout, t);
    std::cout << '\n'; // #1 [ ] Здати домашнє завдання

    return 0;
}

Цей main справді «тонкий»: він створює приклад завдання й викликає функцію друку. Пізніше ви додасте введення, зберігання списку завдань тощо, але структура залишиться тією самою.

Міні‑перевірка в tests/

tests/task_print_smoke.cpp

#include <iostream>
#include "app/task_print.hpp"

int main() {
    app::Task t{.id = 42, .title = "Перевірка друку", .done = true};

    app::print_task(std::cout, t);
    std::cout << '\n'; // #42 [x] Перевірка друку

    return 0;
}

Це не «серйозний юніт‑тест», але корисний «димовий» запуск: ви швидко переконуєтеся, що друк працює, і вам не доводиться запускати весь майбутній інтерфейс застосунку.

3. Типові помилки

Помилка № 1: складати все в src/ і вважати, що заголовки не потрібні.
На перших кроках так справді простіше: один файл, усе поруч. Але щойно вам потрібно повторно використати тип або функцію з іншого .cpp, ви починаєте копіювати оголошення вручну, а потім отримуєте неузгодженість: в одному місці сигнатура змінилася, в іншому — ні. include/ дисциплінує: «оголошення живуть в одному місці».

Помилка № 2: тримати заголовки поруч із .cpp без зрозумілої межі.
Фізично це можливо, але орієнтуватися стає складніше: ви відкриваєте папку й бачите 30 файлів упереміш. Око чіпляється за випадкові речі, і пошук перетворюється на квест. Розділення include/ і src/ — це спосіб зробити межу між «що можна підключати» і «що є реалізацією» очевидною без зайвих пояснень.

Помилка № 3: підключати .cpp через #include, щоб «воно побачилося».
Це майже завжди симптом того, що порушено модель роздільної компіляції, яку ми проходили раніше. .cpp призначені бути окремими одиницями компіляції; підключати їх як текст — означає влаштувати собі дублювання коду й дуже дивні помилки. Якщо вам «не видно функцію», виправляють це заголовком з оголошенням, а не підключенням .cpp.

Помилка № 4: зберігати build/ поруч із вихідними файлами та ще й додавати її до репозиторію або надсилати на перевірку.
Це виглядає спокусливо: «ну там же все вже зібрано!» Але зазвичай такий підхід призводить до того, що проєкт обростає сміттям, а в іншої людини ці файли все одно не підійдуть до її середовища. Нам важливо зберігати вихідні файли, а build/ — відтворювати. Перші кілька разів це психологічно непросто, але потім перетворюється на звичку, яка економить години життя.

Помилка № 5: робити тести, які залазять у «нутрощі» модуля замість того, щоб працювати через його інтерфейс.
Коли перевірки підключають якісь випадкові .cpp або починають залежати від деталей реалізації, ви втрачаєте головний сенс tests/: перевіряти модуль як чорну скриньку. Набагато корисніше, коли в tests/ підключають заголовок із include/ — той самий інтерфейс, яким користується і застосунок.

1
Опитування
Файли проєкту, рівень 26, лекція 4
Недоступний
Файли проєкту
Файли проєкту
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ