CodeGym /Kursy /C++ SELF /target_link_libraries: zależności, linkowanie i tranzytyw...

target_link_libraries: zależności, linkowanie i tranzytywność

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

1. Po co potrzebne jest linkowanie w CMake

Jeśli kompilacja to moment, kiedy kompilator patrzy na pojedynczy plik .cpp i mówi "tak, ten plik jest logicznie poprawny", to linkowanie to moment, kiedy wszystkie kawałki projektu muszą zostać złożone w jeden plik wykonywalny (albo bibliotekę). I tutaj zaczynają się historie w stylu "przecież zrobiłem #include, dlaczego nie działa?!". Dziś uporządkujemy to ostrożnie — bez magii i bez "po prostu włącz jeszcze jedną opcję w IDE".

Wyobraźcie sobie, że projekt to serial. Każdy plik .cpp to oddzielny odcinek, w którym bohaterowie wspominają o sobie nawzajem. Nagłówek (.hpp) to jak "lista postaci i jak wyglądają". Ale żeby serial był prawdziwy, potrzebne są same odcinki (implementacje w .cpp) i potrzebny jest "montażysta", który zmontuje wszystko w jeden sezon. Montażysta to linker.

Na poziomie budowania zwykle występują trzy duże kategorie problemów:

flowchart TD
    A[Błąd CMake] -->|nieprawidłowe polecenie, target nie utworzony| X[configure/generate przerwane]
    B[Błąd kompilacji] -->|syntax/type errors| Y[kompilacja przerwana]
    C[Błąd linkowania] -->|undefined reference / unresolved external| Z[linkowanie przerwane]

To właśnie błędy linkowania najczęściej pojawiają się, gdy zaczynacie robić kilka targetów (aplikacja + biblioteki). Czyli wtedy, gdy CMake staje się naprawdę przydatny.

2. target_link_libraries: sens polecenia

Polecenie target_link_libraries(consumer … dependency …) wygląda przerażająco tylko pierwszy parę razy. Mówiąc prosto, oznacza ono: "target consumer musi być zbudowany razem z targetem dependency, ponieważ potrzebuje jego definicji (implementacji)".

I to ważne: chodzi nie o nagłówki i nie o #include, lecz o implementację — to, co linker umieszcza w finalnym binarium.

Z punktu widzenia "mózgu builda" każdy target ma dwie części:

  • to, co trzeba skompiować (zwykle lista plików .cpp),
  • to, co trzeba dołożyć na etapie linkowania (inne biblioteki/targety).

target_link_libraries zajmuje się drugą częścią.

Minimalny przykład: "powiązać aplikację z biblioteką":

add_library(calc src/calc.cpp)
target_include_directories(calc PUBLIC include)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE calc)

Na tym rysunku app używa funkcji z calc, dlatego na etapie linkowania mówimy: "app, weź też calc".

Bardzo ważny nawyk: we współczesnym CMake linkujemy się z targetem (nazwą celu), a nie ze ścieżkami do plików. Czyli calc, a nie libcalc.a ani calc.cpp. To jest podejście oparte na targetach: zależność to nie "jakiś plik", tylko "cel z właściwościami".

3. Nagłówek ≠ implementacja

Przejdźmy przez typową sytuację początkującego. Napisałeś nagłówek, dołączyłeś go w main.cpp, wszystko ładnie, kompilator zadowolony… i nagle na linkowaniu pojawia się komunikat w stylu undefined reference. Zwykle chce się krzyknąć: "Ale przecież go dołączyłem!".

Ważne rozróżnienie: #include wstawia tekst nagłówka do pliku .cpp na etapie preprocessingu. Pomaga to kompilatorowi zobaczyć deklaracje (declarations): "taka funkcja gdzieś istnieje". Ale #include nie dodaje automatycznie definicji (definition) do builda.

Definicja żyje w pliku .cpp (albo w bibliotece) i musi trafić do targetu, inaczej linker po prostu nie znajdzie ciała funkcji.

Złożymy mały projekt edukacyjny, który będziemy rozwijać w przykładach.

Pliki projektu

MiniCalc/
  CMakeLists.txt
  include/
    calc.hpp
    cli.hpp
  src/
    calc.cpp
    cli.cpp
    main.cpp

Nagłówek z deklaracją

// include/calc.hpp
#pragma once

int add(int a, int b);

Implementacja

// src/calc.cpp
#include "calc.hpp"

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

Użycie w main

// src/main.cpp
#include <iostream>
#include "calc.hpp"

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

Jeśli zapomnisz dodać src/calc.cpp do builda (albo zapomnisz zlinkować bibliotekę, która go zawiera), kompilacja main.cpp przejdzie: kompilator widział int add(int,int);. Ale linkowanie się nie powiedzie: ciała add nie będzie skąd wziąć.

To bezpośrednio wiąże się z faktem, że w C++ istnieje pojęcie linkage (jak nazwy są powiązane między jednostkami translacji).

4. Widoczność zależności: PRIVATE, PUBLIC, INTERFACE

Gdy dodajesz zależność, pojawia się pytanie: kto jeszcze powinien o niej wiedzieć? Tylko sama biblioteka? A może także ten, kto będzie tę bibliotekę używać? Tutaj pojawiają się trzy słowa, które na początku wyglądają jak zaklęcia: PRIVATE, PUBLIC, INTERFACE.

Ustalmy ludzką interpretację: to nie jest o "bezpieczeństwie" ani o private/public w klasach C++. To o rozszerzaniu wymagań builda przez graf zależności targetów.

Warto mieć w głowie tabelkę:

Słowo kluczowe Co oznacza dla consumer -> dependency
PRIVATE
zależność jest potrzebna tylko do zbudowania samego consumer, dalej nie "przekazujemy"
PUBLIC
zależność jest potrzebna i consumer, i wszystkim, którzy będą używać consumer
INTERFACE
samemu consumer zależność nie jest potrzebna (albo jest header-only), ale konsumenci jej potrzebują

Dla target_link_libraries czytamy to jako: "kto musi się zlinkować (lub otrzymać wymagania linkowania)".

Przykład:

target_link_libraries(app PRIVATE calc)

To znaczy: app linkuje się z calc, ale dalej (jeśli ktoś będzie się linkować z app) ta zależność nie musi iść tranzytywnie.

Dla executable'ów często to wystarcza: aplikacja zwykle nie jest "podłączana jako biblioteka". Ale dla bibliotek jest to krytyczne, bo biblioteki istnieją po to, by je używać.

5. Tranzytywność w praktyce

Tranzytywność to straszne słowo, które kryje bardzo prostą ideę: jeśli A zależy od B, a B zależy od C, to czasami A musi zależeć od C, nawet jeśli nie napisałeś tego wprost. CMake może to zrobić za ciebie — ale tylko, jeśli poprawnie oznaczysz widoczność zależności.

Schemat zależności

Chcemy zbudować aplikację app, która używa biblioteki cli, a cli wewnątrz używa calc.

flowchart LR
    app[app executable] --> cli[cli library]
    cli --> calc[calc library]

Teraz pytanie: czy app powinien jawnie linkować się z calc?

Odpowiedź: zależy od tego, co dokładnie cli obiecuje w swoim publicznym interfejsie i jak przebiega linkowanie (szczególnie dla bibliotek statycznych). Na naszym poziomie wystarczy zapamiętać praktyczną zasadę: jeśli cli używa calc, to najczęściej cli powinna linkować się z calc i prawidłowo przekazać tę zależność, aby konsument nie musiał zgadywać.

Praktyczny przykład: app + cli + calc

Złożymy teraz powiązany przykład, w którym app w ogóle nie dołącza calc.hpp. Zna tylko cli.hpp, a cli używa wewnętrznie calc. To klasyczny mini-wątek "wrapper nad biblioteką" — idealne miejsce, żeby poczuć tranzytywność.

Nagłówek cli.hpp

// include/cli.hpp
#pragma once

// cli obiecuje funkcję "obliczyć i ładnie wypisać"
void print_sum(int a, int b);

Implementacja cli.cpp (używa calc)

// src/cli.cpp
#include <iostream>
#include "cli.hpp"
#include "calc.hpp"

void print_sum(int a, int b) {
    std::cout << a << " + " << b << " = " << add(a, b) << '\n';
    // 2 + 3 = 5 (przykład wyjścia)
}

main.cpp zna tylko cli

// src/main.cpp
#include "cli.hpp"

int main() {
    print_sum(2, 3); // 2 + 3 = 5
}

CMake: tworzymy dwie biblioteki i aplikację

cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)

add_library(calc src/calc.cpp)
target_include_directories(calc PUBLIC include)
target_compile_features(calc PRIVATE cxx_std_23)

add_library(cli src/cli.cpp)
target_include_directories(cli PUBLIC include)
target_compile_features(cli PRIVATE cxx_std_23)

# WAŻNE: cli używa calc, więc deklarujemy zależność
target_link_libraries(cli PUBLIC calc)

add_executable(app src/main.cpp)
target_compile_features(app PRIVATE cxx_std_23)

# app linkuje się tylko z cli
target_link_libraries(app PRIVATE cli)

Dlaczego tu PUBLIC, a nie PRIVATE?

Ponieważ chcemy, żeby cli była "normalną" biblioteką, którą można podłączyć jedną linijką. Konsument (app) nie powinien zgadywać, że w środku potrzeba jeszcze calc. Przy PUBLIC CMake widzi graf zależności i potrafi przepuścić wymagania dalej.

Gdyby zrobić tak:

target_link_libraries(cli PRIVATE calc)

projekt może zacząć zachowywać się nieprzewidywalnie: czasem się zbuduje, czasem nie, a czasem — zepsuje się w najmniej oczekiwanym momencie (szczególnie przy dodaniu kolejnych zależności lub zmianie typu biblioteki).

Na starcie kursu wolimy przewidywalność: "podłączyłeś cli — działa".

Jak wybrać PRIVATE lub PUBLIC

Najczęstszy błąd początkującego to oznaczanie wszystkiego jako PUBLIC, "żeby na pewno działało". Rzeczywiście "działa", ale niepostrzeżenie zamieniasz projekt w kłębek zależności: wszystko ściąga się zewsząd, budowanie staje się cięższe, a każda zmiana psuje połowę projektu.

Praktyczny kryterium jest takie: jeśli zależność jest potrzebna dla publicznego interfejsu biblioteki, wtedy jest PUBLIC. Jeśli zależność jest potrzebna tylko wewnątrz implementacji (.cpp) i konsument nie musi jej znać, wtedy PRIVATE.

Jak to rozpoznać bez filozofowania:

  • Jeśli twój nagłówek (include/twoja_biblioteka.hpp) wprost wymaga typów/symboli z innej biblioteki (np. zwracasz typ z zewnętrznej biblioteki, przyjmujesz go jako parametr, lub twój nagłówek musi dołączać cudzy nagłówek), to zależność często staje się częścią interfejsu. Wtedy sensowne jest, aby konsumenci też o niej wiedzieli, i PUBLIC ma sens.
  • Jeśli w nagłówkach pokazujesz tylko własne typy i funkcje, a wewnątrz .cpp używasz cudzej biblioteki jako "narzędzia wewnętrznego", to konsument nie musi o niej wiedzieć. Wtedy PRIVATE utrzymuje zależność lokalną i porządną.

W naszym przykładzie cli.hpp nie dołącza calc.hpp i nie eksponuje funkcji add bezpośrednio. Ale cli nadal musi przy linkowaniu dać znać, że potrzebuje calc, inaczej konsument może niespodziewanie dostać błąd linkowania. Dlatego w modelu edukacyjnym wybieramy PUBLIC jako bardziej niezawodną i "samodzielną" opcję biblioteki.

6. Include‑ścieżki vs linkowanie: jak nie mylić

Teraz praktyczny punkt: dwa błędy wyglądają podobnie ("nie buduje się"), ale leczy się je w różnych miejscach. Dobra wiadomość: gdy nauczysz się rozróżniać je po symptomach, połowa cierpienia z CMake zniknie.

Gdy kompilator wypisuje coś w stylu file not found lub cannot open include file, to oznacza: nie znaleziono nagłówka. Naprawia się to przez target_include_directories.

Gdy linker pisze undefined reference (GCC/Clang) lub unresolved external symbol (MSVC), to oznacza: nie znaleziono definicji (implementacji). Naprawia się to przez target_link_libraries lub dodanie odpowiedniego .cpp do targetu.

Można to trzymać jako mini ściągawkę:

# Nagłówki (żeby kompilowało)
target_include_directories(x PUBLIC include)

# Implementacje/symbole (żeby linkowało)
target_link_libraries(x PUBLIC some_dependency)

I tak, normalne jest, że na początku chce się "wyleczyć linkowanie include'ami" albo "wyleczyć include'y linkowaniem". Prawie wszyscy przez to przechodzą.

7. Typowe błędy przy pracy z target_link_libraries

Błąd nr 1: próbować leczyć undefined reference przez dodanie #include lub katalogów include.
To najczęstsze pomieszanie. Nagłówek pomaga kompilatorowi zobaczyć deklarację, ale na etapie linkowania potrzebne są definicje. Jeśli widzisz błąd linkera, spójrz nie na #include, lecz na listę plików .cpp w target i na target_link_libraries.

Błąd nr 2: zapomnieć, że .hpp sam w sobie "nie bierze udziału w buildzie".
Początkujący czasami myśli: "dodałem include/calc.hpp, więc kalkulator jest podłączony". Nagłówki nie kompilują się same — tylko są dołączane do plików .cpp. Implementacja żyje w .cpp lub w bibliotece i to ona musi trafić do targetu, inaczej linker nie znajdzie symboli.

Błąd nr 3: ustawiać PUBLIC "na wszelki wypadek" i napompować graf zależności.
Gdy wszystko jest oznaczone jako PUBLIC, zależności zaczynają "przeciekać" dalej po projekcie. W małym przykładzie to niegroźne, ale w realnym projekcie nagle każdy target zaczyna ściągać połowę świata. Zdrowszą praktyką jest: domyślnie myśleć o PRIVATE, a PUBLIC wybierać świadomie, gdy zależność jest częścią interfejsu lub chcesz, żeby biblioteka była samodzielna dla konsumentów.

Błąd nr 4: linkować tam, gdzie nie trzeba.
Czasem pisze się target_link_libraries(calc PRIVATE app) zamiast target_link_libraries(app PRIVATE calc). Sens zostaje złamany: zależność powinna iść od konsumenta do dostawcy. Aplikacja używa biblioteki, a nie odwrotnie.

Błąd nr 5: otrzymywać cykliczne zależności między bibliotekami.
Jeśli A linkuje się z B, a B linkuje się z A, projekt zmienia się w kłębek. Czasem coś takiego "jakoś" się zbuduje, ale utrzymywanie tego jest niemożliwe. Na obecnym etapie wystarczy reguła: projektuj zależności w jedną stronę, a wspólny kod wyciągnij do osobnej biblioteki (np. core), tak aby obie strony zależały od niej, zamiast od siebie nawzajem.

1
Zadanie
C++ SELF, poziom 31, lekcja 4
Niedostępne
Dwa moduły
Dwa moduły
1
Zadanie
C++ SELF, poziom 31, lekcja 4
Niedostępne
Biblioteka w nagłówku
Biblioteka w nagłówku
1
Zadanie
C++ SELF, poziom 31, lekcja 4
Niedostępne
Łańcuch zależności
Łańcuch zależności
1
Zadanie
C++ SELF, poziom 31, lekcja 4
Niedostępne
Widoczna zależność
Widoczna zależność
1
Ankieta/quiz
CMake, poziom 31, lekcja 4
Niedostępny
CMake
CMake
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION