CodeGym /Kursy /C++ SELF /target_include_directories

target_include_directories

C++ SELF
Poziom 31, Lekcja 3
Dostępny

1. Ścieżki include w CMake: target_include_directories

Kiedy po raz pierwszy napotykasz błąd w rodzaju fatal error: ...: No such file or directory, chce się winić komputer: „Plik przecież tutaj jest, leży w projekcie!”. Ale kompilator to nie jasnowidz. Szuka nagłówków w oparciu o zestaw katalogów (include paths), i jeśli odpowiedniego folderu nie ma na tej liście, plik jest „niewidoczny”, nawet jeśli fizycznie znajduje się obok.

Ważne jest rozróżnienie dwóch kwestii. Dyrektywa #include w C++ mówi: „potrzebuję tekstu tego nagłówka przy kompilacji”. A to gdzie dokładnie szukać tego tekstu — decydują ustawienia kompilatora. Ręcznie, przez linię poleceń, zwykle jest to flaga -I.... W CMake nie piszemy -I bezpośrednio (przynajmniej w podejściu targetowym), lecz opisujemy ścieżki include przez target_include_directories.

Wyobraź sobie, że kompilator to kurier, któremu dałeś karteczkę „odbierz paczkę z domu calc.hpp”. Jeśli nie powiedziałeś, w których dzielnicach kurier ma szukać, będzie chodził po swoim domyślnym terenie i oczywiście nic nie znajdzie. Nie ma obowiązku sprawdzania całego dysku „na wszelki wypadek” — inaczej budowa każdego projektu trwałaby wiecznie.

Co robi target_include_directories i dlaczego to własność targetu

W CMake ścieżki include są ustawiane nie „dla całego projektu”, lecz dla konkretnego targetu: aplikacji lub biblioteki. To jest podejście targetowe: każdy target ma swoje własne właściwości, a ścieżki include to jedna z ważniejszych, bo wpływają bezpośrednio na kompilację.

Minimalna forma polecenia wygląda tak:

target_include_directories(my_target PRIVATE include)

Sens: przy kompilacji my_target kompilator będzie także szukać nagłówków w katalogu include/ (względem CMakeLists.txt, w którym to definiujesz). Po tym w kodzie C++ możesz pisać #include "math.hpp" (lub #include "calc/add.hpp" — zależnie od struktury folderów), i kompilator odnajdzie plik.

Bardzo praktyczna uwaga: target_include_directories zwykle umieszcza się po add_executable / add_library, ponieważ przed stworzeniem targetu nie ma czego konfigurować.

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

To nie „stylizacja”, a zwykły zdrowy rozsądek: najpierw tworzymy byt, potem nadajemy mu właściwości.

Przykład edukacyjny: biblioteka calc i aplikacja app

Aby temat nie był abstrakcyjny, kontynuujemy jednym przykładem. Zróbmy prostą strukturę projektu:

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

Nagłówek deklaruje funkcję, .cpp implementuje, main.cpp używa. Aplikacja wypisze wynik sumy dwóch liczb (tak, to nie „zabójca Excela”, ale idealnie ćwiczy dyscyplinę budowy projektu).

Plik include/calc/sum.hpp:

#pragma once

int sum(int a, int b);

Plik src/sum.cpp:

#include "calc/sum.hpp"

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

Plik src/main.cpp:

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

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

Teraz pytanie: skąd kompilator wie, że folder include/ w ogóle istnieje i tam powinien szukać calc/sum.hpp? Odpowiedź: z target_include_directories.

Ścieżka include dla aplikacji: scenariusz PRIVATE

Zacznijmy od prostego wariantu, żeby poczuć mechanikę. Załóżmy, że kompilujemy wszystko jednym targetem app (bez oddzielnej biblioteki), i chcemy po prostu, żeby main.cpp widział nagłówki z 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)

Tu PRIVATE czyta się tak: „ścieżka include jest potrzebna tylko do kompilacji tego targetu app”.

Dlaczego to ma sens? Bo app to końcowy plik wykonywalny. Zazwyczaj nie ma on „konsumentów”, którzy podłączaliby go jako bibliotekę. Nie ma więc sensu „przekazywać” ścieżek include dalej.

Ważne jest zauważyć, że src/sum.cpp także używa #include "calc/sum.hpp". I to jest w porządku: ścieżka include stosuje się do wszystkich .cpp, które są kompilowane w ramach targetu.

3. Widoczność ścieżek include: PRIVATE / PUBLIC / INTERFACE

Po co w ogóle PUBLIC i INTERFACE

Do tej pory PRIVATE wydawało się „jedyną słuszną opcją”. Ale to dlatego, że jeszcze nie wykonaliśmy kluczowego kroku: oddzielenia biblioteki od aplikacji.

Gdy masz bibliotekę calc, zwykle chcesz dwóch rzeczy.

Pierwsza: przy budowaniu samej biblioteki kompilator musi znaleźć jej nagłówki.

Druga: gdy inna aplikacja (nasz app) używa biblioteki, również musi znaleźć nagłówki biblioteki — w przeciwnym razie konsument nie będzie w stanie napisać #include "calc/sum.hpp".

I tutaj wchodzi kluczowe słowo dnia: widoczność.

Co oznaczają PUBLIC / PRIVATE / INTERFACE i jak „przechodzą” do konsumenta

Gdy piszesz:

target_include_directories(calc PUBLIC include)

mówisz CMake dwie rzeczy jednocześnie:

1) ścieżka include jest potrzebna samej bibliotece calc, aby się skompilowała (to część „dla siebie”).

2) ścieżka include jest potrzebna wszystkim, którzy podłączą calc, aby mogli używać publicznych nagłówków tej biblioteki (to część „dla konsumentów”).

Dlatego PUBLIC często opisuje się jako „dla siebie i dla innych”.

Aby łatwiej zapamiętać, trzymaj w głowie taką tabelę:

Słowo kluczowe Potrzebne przy kompilacji samego targetu? Potrzebne konsumentom targetu?
PRIVATE
tak nie
PUBLIC
tak tak
INTERFACE
nie tak

Sens INTERFACE na początku wydaje się dziwny: „jak to ścieżka include nie jest potrzebna samemu targetowi?”. Ale jest to bardzo logiczne dla szczególnych celów, np. bibliotek header‑only (gdzie nie ma .cpp) albo targetów „interfejsowych”, które jedynie przekazują ustawienia budowania dalej.

Prawidłowe budowanie calc jako biblioteki: PUBLIC include

Przepiszmy CMake tak, żeby calc stał się oddzielną biblioteką, a app — oddzielną aplikacją, która linkuje z calc (linkowania dziś nie omawiamy szczegółowo, ale sama zależność jest potrzebna do pokazania tranzytywności ścieżek 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)

Zauważ ładny pomysł: app nie musi samodzielnie pisać target_include_directories(app ...) dla nagłówków calc. Dostaje ścieżkę include tranzytywnie, ponieważ calc zadeklarował ją jako PUBLIC.

To jedna z głównych zalet podejścia targetowego: zależności stają się „uczciwe”. Jeśli dołączysz bibliotekę — automatycznie dostajesz wszystko, co potrzebne do jej użycia, ale tylko to, co rzeczywiście zadeklarowano jako publiczne wymaganie.

PRIVATE ścieżka include: nagłówki implementacji, których konsument nie powinien widzieć

Zróbmy teraz projekt nieco bardziej realistycznym. W bibliotece często występują dwa typy nagłówków:

  • Nagłówki publiczne — to, z czego pozwalasz korzystać użytkownikom biblioteki. Zwykle znajdują się w include/.
  • Wewnętrzne nagłówki implementacji — to, czego potrzebujesz tylko wewnątrz biblioteki, a konsumenci lepiej, by tam nie wchodzili.

Zróbmy taką strukturę:

include/
  calc/
    sum.hpp          (publiczny)
src/
  detail/
    sum_impl.hpp     (wewnętrzny)
  sum.cpp

Plik src/detail/sum_impl.hpp:

#pragma once

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

Plik src/sum.cpp:

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

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

Teraz biblioteka calc musi umieć dołączać nagłówki zarówno z include/, jak i z src/ (ponieważ detail/ leży wewnątrz src/). Ale konsument app zdecydowanie nie musi znać detail/sum_impl.hpp.

Tak to wyraża się w CMake:

target_include_directories(calc
    PUBLIC  include
    PRIVATE src
)

Czyta się to tak: „katalog include/ — część publicznego interfejsu, a katalog src/ — wewnętrzna kuchnia, nie wystawiać na zewnątrz”.

Jeśli teraz ktoś w app spróbuje zrobić #include "detail/sum_impl.hpp", prawdopodobnie przestanie się kompilować. I to dobrze: właśnie postawiłeś ogrodzenie wokół szczegółów implementacji.

INTERFACE ścieżka include: biblioteka header‑only

Słowo INTERFACE staje się zrozumiałe, gdy spotykasz bibliotekę bez .cpp. Na przykład masz niewielki zestaw funkcji w nagłówku i chcesz dystrybuować tylko pliki nagłówkowe.

Zróbmy mini‑przykład: biblioteka textutils, która będzie leżeć w include/textutils/trim.hpp, a implementacji w .cpp nie będzie.

include/
  textutils/
    trim.hpp

Plik 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;
}

Tę bibliotekę w CMake można opisać jako target interfejsowy:

add_library(textutils INTERFACE)
target_include_directories(textutils INTERFACE include)

Tu INTERFACE oznacza: „sam target nic nie kompiluje, ale przekazuje konsumentowi ścieżki include i inne wymagania”.

I teraz app może „podłączyć” to tak:

target_link_libraries(app PRIVATE textutils)

Tak, wygląda to nieco nietypowo: jakbyśmy «linkowali» bibliotekę header‑only. Ale w CMake target_link_libraries często oznacza nie tylko „linkowanie” w klasycznym sensie, lecz także dołączanie usage requirements (wymagań użytkowania), do których należą ścieżki include, compile‑definitions i inne właściwości.

Schemat: jak ścieżki include „podróżują” między targetami

Aby nie traktować wszystkiego jak magii, warto wyobrazić to sobie jako przekazywanie wymagań.

flowchart LR
    A[calc target] -- PUBLIC include --> B[app target]
    A -- PRIVATE src --> A
    C[textutils INTERFACE] -- INTERFACE include --> B

Sens schematu: PUBLIC i INTERFACE — to, co „wychodzi na zewnątrz” do konsumenta. PRIVATE zostaje wewnątrz.

Praktyczne zasady wyboru PRIVATE / PUBLIC / INTERFACE

Pisząc pierwszy CMakeLists.txt, łatwo mieć pokusę, by wszędzie ustawić PUBLIC, bo „wtedy na pewno zadziała”. Oczywiście zadziała, ale wkrótce odkryjesz, że twój projekt przypomina kuchnię bez drzwi: zapachy, hałas i garnki w salonie, a goście przypadkowo bawią się chochlą.

Pragmatyczna strategia dla początkujących wygląda tak.

  • Dla pliku wykonywalnego (add_executable) ścieżki include niemal zawsze są PRIVATE, bo plik wykonywalny rzadko bywa używany jako biblioteka.
  • Dla biblioteki (add_library) nagłówki publiczne z katalogu include/ zwykle podaje się jako PUBLIC, bo inaczej konsumenci nie będą mogli korzystać z biblioteki. Wewnętrzne foldery implementacji (src/, src/detail/) zazwyczaj ustawiamy jako PRIVATE, aby konsumenci nie zależeli od szczegółów wewnętrznych.
  • Dla bibliotek header‑only (add_library(name INTERFACE)) ścieżki include ustawiamy jako INTERFACE.

I jeszcze jedna ważna psychologiczna uwaga: PUBLIC to zobowiązanie. Jeśli oznaczysz katalog jako PUBLIC, deklarujesz: „użytkownik biblioteki ma prawo polegać na nagłówkach znajdujących się tam”. A skoro obiecałeś — musisz to utrzymywać.

4. Typowe błędy przy pracy z target_include_directories

Błąd nr 1: leczyć „nie znaleziono nagłówka” przez dodawanie kolejnego #include w kodzie C++.
Gdy kompilator mówi, że nie znalazł calc/sum.hpp, problem zwykle nie polega na tym, że „za mało dołączyłeś”, lecz na tym, że ścieżka wyszukiwania nagłówków nie jest ustawiona. Poprawka powinna znaleźć się w CMakeLists.txt przez target_include_directories, a nie w formie chaotycznego dodawania include'ów w kodzie.

Błąd nr 2: ustawiać PUBLIC „żeby na pewno działało”, nie rozumiejąc, co robisz publicznym.
Jeśli zrobisz target_include_directories(calc PUBLIC src), w praktyce mówisz konsumentom: „dołączaj moje wewnętrzne nagłówki z src/, to normalna część API”. Później zechcesz zmienić nazwę src/detail lub przebudować strukturę wewnętrzną — i nagle złamiesz cudzy kod (albo własne app, jeśli zacznie sięgać do wnętrza). Znacznie bezpieczniej trzymać src/ jako PRIVATE.

Błąd nr 3: oczekiwać, że CMake „sam znajdzie include/”.
CMake nie musi domyślnie zgadywać, co miałeś na myśli. Jeśli masz katalog include/, nie jest to magiczny folder: dopóki go nie dodasz przez target_include_directories, kompilator może go nie znać. Tak, niektóre IDE „podświetlają” nagłówki i nawet robią autouzupełnianie, ale to nie znaczy, że konfiguracja budowy jest poprawna.

Błąd nr 4: mylić ścieżki include ze spisem źródeł.
Dodanie katalogu include to umożliwienie kompilatorowi znalezienia nagłówka. Ale to nie dodaje .cpp do budowy. Dlatego czasem projekt „widzi deklaracje”, ale pada na etapie linkowania (np. undefined reference). To inny typ problemu: nagłówki odpowiadają za kompilację, a obecność .cpp lub bibliotek — za linkowanie.

Błąd nr 5: mieszać „nagłówki publiczne” i „nagłówki implementacji” w jednym kotle.
Jeśli wrzucasz wszystko do include/, konsumenci zaczynają widzieć szczegóły wewnętrzne. Jeśli umieszczasz nagłówki publiczne w src/, musisz udostępnić src/ jako PUBLIC, i sytuacja robi się jeszcze gorsza. Oddzielenie include/ (interfejs) od src/ (implementacja) może wydawać się pedantyczne, ale to jedno z najbardziej opłacalnych pedantyzmów w C++.

1
Zadanie
C++ SELF, poziom 31, lekcja 3
Niedostępne
Nagłówek obok
Nagłówek obok
1
Zadanie
C++ SELF, poziom 31, lekcja 3
Niedostępne
Interfejs publiczny
Interfejs publiczny
1
Zadanie
C++ SELF, poziom 31, lekcja 3
Niedostępne
Ukryte szczegóły
Ukryte szczegóły
1
Zadanie
C++ SELF, poziom 31, lekcja 3
Niedostępne
Nagłówek bez cpp
Nagłówek bez cpp
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION