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 и почему это свойство target’а

В 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, потому что до создания target’а настраивать нечего.

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

Начнём с варианта «в лоб», чтобы почувствовать механику. Допустим, мы пока собираем всё одним target’ом 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, которые компилируются в составе target’а.

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 часто описывают как «и себе, и другим».

Чтобы было проще запомнить, держите в голове такую табличку:

Ключевое слово Нужно при сборке самого target’а? Нужно потребителям target’а?
PRIVATE
да нет
PUBLIC
да да
INTERFACE
нет да

Смысл INTERFACE сначала кажется странным: «как это include‑путь не нужен самому target’у?». Но он очень логичен для особых целей, например, header‑only библиотек (где нет .cpp) или «интерфейсных» targets, которые только передают настройки сборки дальше.

Правильная сборка 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. Например, у вас есть небольшой набор функций в заголовке, и вы хотите распространять только headers.

Сделаем мини‑пример: библиотека 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 такую библиотеку можно описать интерфейсным target’ом:

add_library(textutils INTERFACE)
target_include_directories(textutils INTERFACE include)

Здесь INTERFACE означает: «сам target ничего не компилирует, но передаёт потребителю include‑пути и прочие требования».

И теперь app может “подключить” это так:

target_link_libraries(app PRIVATE textutils)

Да, выглядит немного непривычно: мы как будто «линкуем» header‑only библиотеку. Но в CMake target_link_libraries часто означает не только “линковку в классическом смысле”, а ещё и подключение usage requirements (требований использования), куда входят include‑пути, compile‑definitions и другие свойства.

Схема: как include‑пути «путешествуют» между targets

Чтобы не держать всё в голове как магию, полезно представить это как передачу требований.

flowchart LR
    A[calc target] -- PUBLIC include --> B[app target]
    A -- PRIVATE src --> A
    C[textutils INTERFACE] -- 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++.

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