JavaRush /Курси /C++ SELF /target_include_directories

target_include_directories

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

1. Include‑шляхи в CMake: target_include_directories

Коли ви вперше натрапляєте на помилку на кшталт fatal error: ...: No such file or directory, легко образитися на компʼютер: «Файл же ось він, у проєкті!». Але компілятор — не екстрасенс. Він шукає заголовки за набором каталогів (include paths). Якщо потрібної папки в цьому списку немає, файл вважається «невидимим», навіть коли він буквально лежить поруч на диску.

Важливо розрізняти дві речі. Директива #include у C++ каже: «мені потрібен текст цього заголовка під час компіляції». А от де саме шукати цей текст, визначають налаштування компілятора. Якщо робити це вручну через командний рядок, зазвичай використовують прапорець -I.... У CMake ми не пишемо -I напряму, принаймні в target‑підході, а описуємо include‑шляхи через target_include_directories.

Уявіть, що компілятор — це курʼєр, якому ви дали записку: «забери посилку з дому calc.hpp». Якщо ви не сказали, у яких районах курʼєр може шукати, він ходитиме своїм стандартним маршрутом і, звісно, нічого не знайде. Він не зобовʼязаний обходити весь ваш компʼютер «про всяк випадок» — інакше збирання будь-якого проєкту тривало б вічність.

Що робить target_include_directories і чому це властивість цілі

У CMake include‑шляхи задають не «загалом для проєкту», а для конкретної цілі: застосунку або бібліотеки. У цьому й полягає target‑підхід: кожна ціль має власні властивості, і include‑шляхи — одна з найважливіших серед них, бо вони безпосередньо впливають на компіляцію.

Мінімальна форма команди має такий вигляд:

target_include_directories(my_target PRIVATE include)

Сенс простий: під час компіляції my_target компілятор шукатиме заголовки ще й у папці include/ (відносно CMakeLists.txt, де ви це пишете). Після цього в C++‑коді можна писати #include "math.hpp" (або #include "calc/add.hpp" — залежно від структури папок), і компілятор знайде файл.

Практичний момент: target_include_directories зазвичай ставлять після add_executable / add_library, тому що до створення цілі налаштовувати ще нічого.

add_executable(app src/main.cpp)
target_include_directories(app PRIVATE include)

Це не просто «стилістика», а здоровий глузд: спочатку створюємо сутність, а потім задаємо їй властивості.

Навчальний приклад: бібліотека calc і застосунок app

Щоб тема не залишалася абстрактною, продовжимо той самий приклад. Зробімо просту структуру проєкту:

MiniProject/
  CMakeLists.txt
  include/
    calc/
      sum.hpp
  src/
    sum.cpp
    main.cpp

Заголовок оголошує функцію, .cpp її реалізує, а main.cpp використовує. Сам застосунок друкуватиме результат суми двох чисел. Так, це не «вбивця Excel», зате такий приклад ідеально тренує дисципліну збирання.

Файл include/calc/sum.hpp:

#pragma once

int sum(int a, int b);

Файл src/sum.cpp:

#include "calc/sum.hpp"

int sum(int a, int b) {
    return a + b;
}

Файл src/main.cpp:

#include <iostream>
#include "calc/sum.hpp"

int main() {
    std::cout << sum(2, 3) << '\n'; // 5
}

Тепер питання: звідки компілятор дізнається, що папка include/ узагалі існує і що саме там потрібно шукати calc/sum.hpp? Відповідь: із target_include_directories.

Include‑шлях для застосунку: сценарій PRIVATE

Почнімо з найпростішого варіанта, щоб відчути механіку. Припустімо, поки що ми збираємо все однією ціллю app (без окремої бібліотеки) і просто хочемо, щоб main.cpp бачив заголовки з include/.

cmake_minimum_required(VERSION 3.20)
project(MiniProject LANGUAGES CXX)

add_executable(app
    src/main.cpp
    src/sum.cpp
)

target_compile_features(app PRIVATE cxx_std_23)
target_include_directories(app PRIVATE include)

Тут PRIVATE читається так: «include‑шлях потрібен лише для збирання цієї цілі app».

Чому це логічно? Тому що app — кінцевий виконуваний файл. Зазвичай у нього немає «споживачів», які підключатимуть його як бібліотеку. Отже, передавати include‑шляхи далі просто немає сенсу.

Важливо зауважити, що src/sum.cpp теж використовує #include "calc/sum.hpp". І це нормально: include‑шлях застосовується до усіх .cpp, які компілюються у складі цілі.

3. Видимість include‑шляхів: PRIVATE / PUBLIC / INTERFACE

Навіщо взагалі потрібні PUBLIC і INTERFACE

До цього моменту PRIVATE може здаватися «єдино правильним варіантом». Але лише тому, що ми ще не зробили головного кроку: не розділили проєкт на бібліотеку і застосунок.

Коли у вас зʼявляється бібліотека calc, зазвичай потрібні дві речі.

Перша: під час збирання самої бібліотеки компілятор повинен знаходити її заголовки.

Друга: коли інший застосунок, тобто наш app, використовує бібліотеку, він теж повинен знаходити її заголовки — інакше споживач не зможе написати #include "calc/sum.hpp".

І ось тут зʼявляється ключове слово дня: видимість.

Що означають PUBLIC / PRIVATE / INTERFACE і як вони передаються споживачеві

Коли ви пишете:

target_include_directories(calc PUBLIC include)

ви одночасно повідомляєте CMake дві речі:

1) include‑шлях потрібен самій бібліотеці calc, щоб вона компілювалася (це частина «для себе»).

2) include‑шлях потрібен усім, хто підключить calc, щоб вони могли використовувати публічні заголовки цієї бібліотеки (це частина «для споживачів»).

Саме тому PUBLIC часто описують як «і собі, і іншим».

Щоб це було простіше запамʼятати, тримайте в голові таку таблицю:

Ключове слово Потрібно під час збирання самої цілі? Потрібно споживачам цілі?
PRIVATE
так ні
PUBLIC
так так
INTERFACE
ні так

Зміст INTERFACE спочатку може здаватися дивним: «як це include‑шлях не потрібен самій цілі?». Але він цілком логічний для особливих випадків, наприклад для header‑only бібліотек (де немає .cpp) або «інтерфейсних» цілей, які лише передають налаштування збирання далі.

Правильне збирання calc як бібліотеки: PUBLIC include

Перепишімо CMake так, щоб calc став окремою бібліотекою, а app — окремим застосунком, який лінкується з calc (лінкування ми сьогодні глибоко не розбиратимемо, але сам факт залежності нам потрібен, щоб показати транзитивність include‑шляхів).

cmake_minimum_required(VERSION 3.20)
project(MiniProject LANGUAGES CXX)

add_library(calc
    src/sum.cpp
)

target_compile_features(calc PRIVATE cxx_std_23)
target_include_directories(calc PUBLIC include)

add_executable(app
    src/main.cpp
)

target_compile_features(app PRIVATE cxx_std_23)
target_link_libraries(app PRIVATE calc)

Зверніть увагу на важливу ідею: app не зобовʼязаний писати target_include_directories(app ...) для заголовків calc. Він отримує include‑шлях транзитивно, тому що calc оголосив його як PUBLIC.

Це одна з головних причин полюбити target‑підхід: залежності стають «чесними». Якщо ви підключили бібліотеку, то автоматично отримали все, що потрібно для її використання, — але лише те, що справді оголошено як публічна вимога.

PRIVATE include‑шлях: заголовки реалізації, які споживач бачити не повинен

Тепер зробімо проєкт трохи реалістичнішим. У бібліотеці часто є заголовки двох типів:

  • Публічні заголовки — те, що ви дозволяєте використовувати користувачам бібліотеки. Зазвичай вони лежать у include/.
  • Внутрішні заголовки реалізації — те, що потрібно лише вам усередині бібліотеки, але споживачам краще туди не лізти.

Зробімо таку структуру:

include/
  calc/
    sum.hpp          (публічний)
src/
  detail/
    sum_impl.hpp     (внутрішній)
  sum.cpp

Файл src/detail/sum_impl.hpp:

#pragma once

inline int sum_impl(int a, int b) {
    return a + b;
}

Файл src/sum.cpp:

#include "calc/sum.hpp"
#include "detail/sum_impl.hpp"

int sum(int a, int b) {
    return sum_impl(a, b);
}

Тепер для бібліотеки calc потрібно, щоб компілятор знаходив заголовки і з include/, і з src/ (бо detail/ лежить усередині src/). Але споживачеві app точно не потрібно знати про detail/sum_impl.hpp.

Ось як це виражається в CMake:

target_include_directories(calc
    PUBLIC  include
    PRIVATE src
)

Читається це так: «папка include/ — частина публічного інтерфейсу, а папка src/ — внутрішня кухня, не виносити в зал».

Якщо тепер хтось у app спробує зробити #include "detail/sum_impl.hpp", це, найімовірніше, вже не скомпілюється. І це чудово: ви щойно поставили паркан навколо внутрішніх деталей.

INTERFACE include‑шлях: header‑only бібліотека

Слово INTERFACE зазвичай стає зрозумілим, коли ви натрапляєте на бібліотеку без .cpp. Наприклад, у вас є невеликий набір функцій у заголовку, і ви хочете поширювати лише заголовки.

Зробімо мініприклад: бібліотека textutils, яка лежатиме в include/textutils/trim.hpp, а реалізації в .cpp не матиме.

include/
  textutils/
    trim.hpp

Файл include/textutils/trim.hpp:

#pragma once
#include <string>

inline std::string trim_one_space(std::string s) {
    if (!s.empty() && s.front() == ' ') s.erase(s.begin());
    if (!s.empty() && s.back()  == ' ') s.pop_back();
    return s;
}

У CMake таку бібліотеку можна описати інтерфейсною ціллю:

add_library(textutils INTERFACE)
target_include_directories(textutils INTERFACE include)

Тут INTERFACE означає: «сама ціль нічого не компілює, але передає споживачеві include‑шляхи та інші вимоги».

І тепер app може «підключити» це так:

target_link_libraries(app PRIVATE textutils)

Так, це виглядає трохи незвично: ми ніби «лінкуємо» header‑only бібліотеку. Але в CMake target_link_libraries часто означає не лише «лінкування в класичному сенсі», а й підключення usage requirements (вимог використання), куди входять include‑шляхи, compile‑definitions та інші властивості.

Схема: як include‑шляхи «подорожують» між цілями

Щоб не сприймати все це як магію, корисно уявляти процес як передавання вимог.

flowchart LR
    A[ціль calc] -- PUBLIC include --> B[ціль app]
    A -- PRIVATE src --> A
    C[інтерфейсна ціль textutils] -- INTERFACE include --> B

Сенс схеми такий: PUBLIC і INTERFACE — це те, що передається назовні, споживачеві. PRIVATE лишається всередині.

Практичні правила вибору PRIVATE / PUBLIC / INTERFACE

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

Прагматична стратегія для новачків така:

  • Для виконуваного файла (add_executable) include‑шляхи майже завжди PRIVATE, тому що виконуваний файл рідко хтось «використовує як бібліотеку».
  • Для бібліотеки (add_library) публічні заголовки з папки include/ зазвичай підключають як PUBLIC, бо інакше споживачі не зможуть користуватися бібліотекою. Внутрішні папки реалізації (src/, src/detail/) зазвичай підключають як PRIVATE, щоб споживачі не залежали від внутрішньої кухні.
  • Для header‑only бібліотек (add_library(name INTERFACE)) include‑шляхи задають як INTERFACE.

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

4. Типові помилки під час роботи з target_include_directories

Помилка № 1: лікувати «заголовок не знайдено» додаванням ще одного #include у C++‑код.
Коли компілятор повідомляє, що не знайшов calc/sum.hpp, проблема зазвичай не в тому, що ви «мало підключили», а в тому, що шлях пошуку заголовків не налаштований. Виправляти це треба в CMakeLists.txt через target_include_directories, а не хаотично додавати includeʼи.

Помилка № 2: ставити PUBLIC «щоб точно працювало», не розуміючи, що саме ви робите публічним.
Якщо ви зробили target_include_directories(calc PUBLIC src), ви фактично сказали споживачам: «включайте мої внутрішні заголовки з src/, це нормальна частина API». Потім ви захочете перейменувати src/detail або перебудувати внутрішню структуру — і раптом зламаєте чужий код (або навіть свій app, якщо він почав лізти у внутрішності). Значно безпечніше залишати src/ як PRIVATE.

Помилка № 3: очікувати, що CMake «сам знайде include/».
CMake не зобовʼязаний здогадуватися, що ви мали на увазі. Якщо у вас є include/, це не якась магічна папка: доки ви не додали її через target_include_directories, компілятор може про неї не знати. Так, деякі IDE «підсвічують» заголовки й навіть роблять автодоповнення, але це не означає, що збирання налаштовано правильно.

Помилка № 4: плутати include‑шляхи і список вихідних файлів.
Додати include‑директорію — це зробити так, щоб компілятор міг знайти заголовок. Але це не додає .cpp до збирання. Тому інколи проєкт «бачить оголошення», але падає на лінкуванні (наприклад, undefined reference). Це інший клас проблем: заголовки відповідають за компіляцію, а участь .cpp або бібліотек — за лінкування.

Помилка № 5: змішувати «публічні заголовки» і «заголовки реалізації» в одну купу.
Якщо ви складаєте все підряд у include/, споживачі починають бачити внутрішні деталі. Якщо ж ви кладете публічні заголовки в src/, вам доводиться роздавати src/ як PUBLIC, і ситуація стає ще гіршою. Відокремлення include/ (інтерфейс) від src/ (реалізація) може здаватися занудством, але це одне з найкорисніших занудств у C++.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ