Skip to content

Latest commit

 

History

History
478 lines (349 loc) · 22.1 KB

File metadata and controls

478 lines (349 loc) · 22.1 KB

🔝 Retour au Sommaire

26.2.4 PUBLIC, PRIVATE, INTERFACE

Objectif : Consolider la compréhension du système de visibilité de CMake en synthétisant les concepts vus dans les sections précédentes, en explorant les scénarios avancés de propagation, les erreurs courantes, et en fournissant un arbre de décision applicable à tous les cas de figure.


Pourquoi une section dédiée ?

Les trois mots-clés PUBLIC, PRIVATE et INTERFACE sont apparus tout au long des sections 26.2 à 26.2.3 — dans target_link_libraries(), target_include_directories(), target_compile_definitions(), target_compile_options(), et target_compile_features(). Chaque section les a abordés sous l'angle de la commande concernée. Cette section les traite comme un système unifié, car c'est exactement ce qu'ils sont : un seul mécanisme de propagation, appliqué de manière cohérente à toutes les propriétés d'une cible.


Le modèle unifié : deux ensembles de propriétés

Pour comprendre la visibilité, il faut se représenter chaque cible CMake comme portant deux ensembles de propriétés :

  • L'ensemble build : les propriétés nécessaires pour compiler la cible elle-même.
  • L'ensemble usage (usage requirements) : les propriétés propagées aux cibles qui dépendent de celle-ci.

Les trois mots-clés contrôlent dans quel(s) ensemble(s) une propriété est placée :

Mot-clé Ensemble build Ensemble usage La cible elle-même l'utilise ? Les consommateurs en héritent ?
PRIVATE
PUBLIC
INTERFACE

PUBLIC = PRIVATE + INTERFACE. C'est littéralement l'union des deux : la propriété est utilisée pour le build de la cible et propagée aux consommateurs.

Ce modèle s'applique identiquement à toutes les commandes target_*. Il n'y a pas de cas particulier pour les includes versus les définitions versus les dépendances. La mécanique est la même partout :

# Même logique de visibilité, commandes différentes
target_include_directories(lib  PUBLIC  /path/to/headers)    # Build + Usage  
target_compile_definitions(lib  PRIVATE INTERNAL_FLAG=1)     # Build uniquement  
target_compile_features(lib     INTERFACE cxx_std_23)        # Usage uniquement  
target_link_libraries(lib       PUBLIC  Threads::Threads)    # Build + Usage  
target_compile_options(lib      PRIVATE -Wconversion)        # Build uniquement  

Arbre de décision

Face à chaque propriété que vous attachez à une cible, posez-vous les questions suivantes dans l'ordre :

La cible a-t-elle des fichiers sources à compiler ?
│
├── NON (bibliothèque INTERFACE / header-only)
│   └── → Tout est INTERFACE. Pas d'autre choix possible.
│
└── OUI (bibliothèque STATIC/SHARED/OBJECT ou exécutable)
    │
    Cette propriété est-elle visible dans les headers publics de la cible ?
    │
    ├── OUI (un type exposé, un #include dans un header public,
    │        un flag nécessaire pour compiler le code qui inclut ces headers)
    │   └── → PUBLIC
    │
    └── NON (utilisée uniquement dans les .cpp, ou dans des headers internes)
        └── → PRIVATE

Quelques exemples concrets d'application de cet arbre :

Propriété Visible dans les headers publics ? Visibilité
Include directory vers include/my_project/ Oui — c'est l'API PUBLIC
Include directory vers src/internal/ Non — implémentation PRIVATE
-DUSE_OPENSSL=1 utilisé dans un #ifdef d'un header public Oui PUBLIC
-DLOG_LEVEL=3 utilisé uniquement dans un .cpp Non PRIVATE
Dépendance Threads::Threads dont std::mutex apparaît dans un header public Oui PUBLIC
Dépendance ZLIB::ZLIB utilisée uniquement dans compression.cpp Non PRIVATE
-Wall -Wextra (flags de warning) Non — n'affecte pas l'API PRIVATE
cxx_std_23 quand les headers utilisent des features C++23 Oui PUBLIC
cxx_std_23 quand seuls les .cpp utilisent C++23 Non PRIVATE

Propagation transitive : le graphe complet

Reprenons un graphe de dépendances réaliste pour observer la propagation en action. Quatre cibles, trois niveaux de profondeur :

# ── Niveau 3 : bibliothèque fondation ──
add_library(foundation STATIC foundation.cpp)  
target_include_directories(foundation PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)  
target_compile_definitions(foundation  
    PUBLIC  FOUNDATION_VERSION=3
    PRIVATE FOUNDATION_INTERNAL_CHECK=1
)

# ── Niveau 2 : bibliothèque réseau, dépend de foundation ──
add_library(network STATIC network.cpp)  
target_link_libraries(network  
    PUBLIC  foundation          # foundation est exposée dans l'API de network
    PRIVATE OpenSSL::SSL        # OpenSSL est un détail d'implémentation
)
target_compile_definitions(network
    PUBLIC  HAS_NETWORKING=1
    PRIVATE NET_BUFFER_SIZE=8192
)

# ── Niveau 1 : application, dépend de network ──
add_executable(server main.cpp)  
target_link_libraries(server PRIVATE network)  

Que reçoit server après résolution complète ? Parcourons le graphe :

Depuis network (dépendance directe, PRIVATE) : server reçoit les exigences d'usage de network. C'est-à-dire les propriétés déclarées PUBLIC et INTERFACE dans network :

  • Définition HAS_NETWORKING=1 (PUBLIC dans network)
  • La dépendance foundation (PUBLIC dans network) → résolution récursive

Depuis foundation (dépendance transitive, via network PUBLIC) : server reçoit les exigences d'usage de foundation :

  • Include directory de foundation (PUBLIC)
  • Définition FOUNDATION_VERSION=3 (PUBLIC dans foundation)

Ce que server ne reçoit PAS :

  • FOUNDATION_INTERNAL_CHECK=1 — PRIVATE dans foundation
  • NET_BUFFER_SIZE=8192 — PRIVATE dans network
  • OpenSSL::SSL et toutes ses propriétés — PRIVATE dans network

Le résultat en flags de compilation pour server :

g++ -DFOUNDATION_VERSION=3 -DHAS_NETWORKING=1 \
    -I/path/to/foundation/include \
    -c main.cpp -o main.o

Et en flags de linkage :

g++ main.o -lnetwork -lfoundation -lssl -lcrypto -lpthread -o server

Notez qu'OpenSSL apparaît dans la ligne de linkage même s'il est PRIVATE — le linker en a besoin pour résoudre les symboles. Mais les propriétés de compilation d'OpenSSL (include directories, définitions) ne sont pas propagées à server. C'est la distinction subtile entre propagation des propriétés de compilation et résolution des symboles au linkage.


Le cas des exécutables : PRIVATE suffit presque toujours

Un exécutable est une cible terminale dans le graphe de dépendances — rien ne dépend d'un exécutable. Par conséquent, la distinction PUBLIC/PRIVATE perd de son importance : il n'y a aucun consommateur à qui propager quoi que ce soit.

Par convention et par clarté d'intention, utilisez PRIVATE pour toutes les propriétés d'un exécutable :

add_executable(my_app main.cpp)

target_link_libraries(my_app PRIVATE my_project::core)  
target_include_directories(my_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/app_config)  
target_compile_definitions(my_app PRIVATE APP_VERSION="1.2.0")  
target_compile_options(my_app PRIVATE -Wall -Wextra)  

Techniquement, PUBLIC fonctionnerait aussi (les propriétés iraient dans l'ensemble usage, mais personne ne les consommerait). Cependant, PRIVATE communique l'intention correcte : ces propriétés sont pour la compilation de l'exécutable, point final.

L'exception théorique serait un exécutable utilisé comme dépendance d'une cible personnalisée (add_custom_command), mais c'est un cas extrêmement rare qui ne justifie pas de changer la convention.


Erreurs courantes et pièges

Piège n°1 : tout mettre en PUBLIC « par sécurité »

C'est l'erreur la plus répandue chez les débutants. Quand on ne sait pas quel mot-clé choisir, on met PUBLIC partout — « au moins ça marche ». Le build fonctionne effectivement, mais les conséquences sont réelles.

Temps de compilation allongés. Chaque include directory et définition PUBLIC se propage à toutes les cibles en aval. Si foundation expose en PUBLIC un include directory vers un répertoire contenant des centaines de headers complexes, toutes les cibles qui dépendent (même indirectement) de foundation verront leurs temps de résolution d'includes augmenter.

Recompilations en cascade inutiles. Si un header dans un répertoire PUBLIC est modifié, toutes les cibles en aval sont potentiellement invalidées par le build system. Si ce header n'est en réalité utilisé que dans les .cpp de la cible (et aurait dû être PRIVATE), les recompilations en cascade sont du gaspillage pur.

Fuites de détails d'implémentation. Un consommateur de votre bibliothèque se retrouve avec des macros, des chemins d'inclusion et des dépendances qu'il n'a pas demandés. Cela peut provoquer des conflits de noms, des ambiguïtés d'inclusion, ou des dépendances implicites difficiles à déboguer.

# ❌ Surexposition — tout le monde hérite d'OpenSSL et des warnings
target_link_libraries(my_lib PUBLIC OpenSSL::SSL)  
target_compile_options(my_lib PUBLIC -Wall -Wextra -Wpedantic)  

# ✅ Encapsulation — seul my_lib voit OpenSSL et les warnings
target_link_libraries(my_lib PRIVATE OpenSSL::SSL)  
target_compile_options(my_lib PRIVATE -Wall -Wextra -Wpedantic)  

Piège n°2 : PRIVATE quand PUBLIC est nécessaire

L'erreur inverse produit des erreurs de compilation chez le consommateur. Symptôme typique : un header public de votre bibliothèque inclut un type d'une dépendance, mais cette dépendance est déclarée PRIVATE. Le consommateur compile, le compilateur ouvre votre header, tombe sur un type inconnu, et produit une erreur souvent cryptique.

// include/my_project/network.h
#pragma once
#include <openssl/ssl.h>    // ← Type OpenSSL dans un header PUBLIC

class SecureChannel {
    SSL_CTX* ctx_;           // ← Le consommateur doit connaître SSL_CTX
public:
    void connect(const std::string& host);
};
# ❌ OpenSSL est utilisé dans le header public mais déclaré PRIVATE
target_link_libraries(my_project_core PRIVATE OpenSSL::SSL)
# Le consommateur obtiendra : fatal error: openssl/ssl.h: No such file or directory

La solution est de remonter la visibilité en PUBLIC, ou mieux, de repenser le design pour cacher le type OpenSSL derrière un Pimpl (pointer to implementation) ou un type opaque :

// Solution 1 : remonter en PUBLIC
// → Simple mais expose OpenSSL dans l'API
target_link_libraries(my_project_core PUBLIC OpenSSL::SSL)
// Solution 2 : Pimpl — garder OpenSSL en PRIVATE
// include/my_project/network.h
#pragma once
#include <memory>
#include <string>

class SecureChannel {
    struct Impl;                         // Déclaration forward
    std::unique_ptr<Impl> impl_;         // Pas de dépendance sur OpenSSL ici
public:
    SecureChannel();
    ~SecureChannel();
    void connect(const std::string& host);
};
// src/network.cpp
#include <openssl/ssl.h>                 // OpenSSL isolé dans le .cpp
#include <my_project/network.h>

struct SecureChannel::Impl {
    SSL_CTX* ctx = nullptr;
};
// ...

La solution 2 est architecturalement supérieure : la dépendance OpenSSL reste un détail d'implémentation invisible pour le consommateur, et le mot-clé PRIVATE est correct.

Piège n°3 : oublier la visibilité (ancien style)

# ❌ Pas de mot-clé — comportement ambigu
target_link_libraries(my_lib Boost::filesystem)

Sans mot-clé explicite, CMake entre en mode de compatibilité dont le comportement dépend du contexte (voir section 26.2.2). Spécifiez toujours la visibilité, même quand vous pensez que le choix est évident.

Piège n°4 : visibilités incohérentes entre commandes

Chaque propriété a sa propre visibilité, indépendante des autres. Rien n'empêche de lier une dépendance en PUBLIC mais de déclarer son include directory en PRIVATE. Cela n'a pas de sens et produit des configurations incohérentes :

# ❌ Incohérent — la dépendance est PUBLIC mais les includes sont PRIVATE
target_link_libraries(my_lib PUBLIC some_dep)  
target_include_directories(my_lib PRIVATE ${SOME_DEP_INCLUDE})  

En pratique, ce piège est rare quand on utilise des cibles importées (la propagation est automatique). Il apparaît surtout quand on gère manuellement les include directories de bibliothèques tierces — raison de plus pour toujours passer par des cibles CMake.


Interaction avec les types de bibliothèques

Le type de la bibliothèque contraint les visibilités valides :

Bibliothèque STATIC ou SHARED

Les trois visibilités sont disponibles. C'est le cas standard couvert tout au long de cette section.

Bibliothèque OBJECT

Les trois visibilités sont disponibles depuis CMake 3.12. Le comportement est identique aux bibliothèques STATIC/SHARED, à ceci près que les fichiers objets (.o) sont propagés directement aux cibles qui en dépendent, sans passer par une archive .a ou un .so.

Bibliothèque INTERFACE

Seul INTERFACE est valide. C'est logique : la cible ne compile rien, donc il n'y a pas d'ensemble build. Toutes les propriétés sont exclusivement destinées aux consommateurs.

add_library(my_header_lib INTERFACE)

# ✅ INTERFACE est le seul choix
target_include_directories(my_header_lib INTERFACE ${PROJECT_SOURCE_DIR}/include)  
target_compile_features(my_header_lib INTERFACE cxx_std_23)  
target_link_libraries(my_header_lib INTERFACE Boost::headers)  

# ❌ Erreur CMake — PUBLIC et PRIVATE interdits pour une cible INTERFACE
target_include_directories(my_header_lib PUBLIC ${PROJECT_SOURCE_DIR}/include)  
target_compile_options(my_header_lib PRIVATE -Wall)  

Pattern avancé : cibles INTERFACE comme collections de propriétés

Les cibles INTERFACE ne sont pas limitées aux bibliothèques header-only. Elles peuvent servir de véhicules de propriétés — des cibles qui ne produisent rien mais transportent un ensemble cohérent de configurations. Plusieurs patterns utiles en découlent.

Cible de warnings du projet

add_library(project_warnings INTERFACE)  
target_compile_options(project_warnings INTERFACE  
    $<$<CXX_COMPILER_ID:GNU>:
        -Wall -Wextra -Wpedantic -Wshadow -Wconversion
        -Wnon-virtual-dtor -Wold-style-cast -Woverloaded-virtual
    >
    $<$<CXX_COMPILER_ID:Clang>:
        -Wall -Wextra -Wpedantic -Wshadow -Wconversion
        -Wnon-virtual-dtor -Wold-style-cast -Woverloaded-virtual
        -Wno-unknown-warning-option
    >
)

Chaque cible du projet lie contre project_warnings en PRIVATE :

target_link_libraries(my_project_core PRIVATE project_warnings)  
target_link_libraries(my_project_utils PRIVATE project_warnings)  
target_link_libraries(my_project_app PRIVATE project_warnings)  

Centralisation parfaite : un seul endroit à modifier pour ajuster la politique de warnings de tout le projet. Et comme le linkage est PRIVATE, les warnings ne polluent pas les consommateurs externes.

Cible d'options de sanitizers

add_library(project_sanitizers INTERFACE)

option(ENABLE_ASAN "Activer AddressSanitizer" OFF)  
option(ENABLE_TSAN "Activer ThreadSanitizer" OFF)  

if(ENABLE_ASAN)
    target_compile_options(project_sanitizers INTERFACE -fsanitize=address -fno-omit-frame-pointer)
    target_link_options(project_sanitizers INTERFACE -fsanitize=address)
endif()

if(ENABLE_TSAN)
    target_compile_options(project_sanitizers INTERFACE -fsanitize=thread)
    target_link_options(project_sanitizers INTERFACE -fsanitize=thread)
endif()
# Appliqué à toutes les cibles
target_link_libraries(my_project_core PRIVATE project_sanitizers)  
target_link_libraries(my_project_app PRIVATE project_sanitizers)  

Cible de configuration globale du projet

add_library(project_config INTERFACE)  
target_compile_features(project_config INTERFACE cxx_std_23)  
target_compile_definitions(project_config INTERFACE  
    $<$<CONFIG:Debug>:MY_PROJECT_DEBUG=1>
    $<$<CONFIG:Release>:MY_PROJECT_RELEASE=1>
)

Ce pattern remplace avantageusement les variables globales CMAKE_CXX_STANDARD et add_definitions() : les propriétés sont portées par une cible, se propagent de manière contrôlée, et n'affectent pas les sous-projets tiers.


Propagation et FetchContent / add_subdirectory

Quand vous intégrez un projet tiers via FetchContent ou add_subdirectory, ses cibles rejoignent votre graphe de dépendances. La visibilité interagit alors avec le code CMake du projet tiers, que vous ne contrôlez pas forcément.

Scénario problématique

Un projet tiers déclare des options de compilation en PUBLIC :

# CMakeLists.txt du projet tiers (vous ne le contrôlez pas)
add_library(third_lib STATIC ...)  
target_compile_options(third_lib PUBLIC -Werror)  # ← Aïe  

Si vous liez contre third_lib :

target_link_libraries(my_project_core PRIVATE third_lib)

Le -Werror se propage à my_project_core via les exigences d'usage de third_lib. Votre code, qui compilait sans erreur avec des warnings, échoue soudainement.

Solutions

La solution la plus propre est de signaler le problème au projet tiers (un -Werror en PUBLIC est une mauvaise pratique). En attendant, vous pouvez neutraliser la propagation en ajoutant explicitement l'option contraire après le linkage :

target_link_libraries(my_project_core PRIVATE third_lib)  
target_compile_options(my_project_core PRIVATE -Wno-error)  

Ou, si le projet tiers le permet, désactiver le comportement via une option CMake :

set(THIRD_LIB_WERROR OFF CACHE BOOL "" FORCE)  
FetchContent_Declare(third_lib ...)  
FetchContent_MakeAvailable(third_lib)  

Ce type de friction est une motivation supplémentaire pour que vos propres bibliothèques appliquent la visibilité minimale : les warnings et les flags de développement doivent toujours être PRIVATE.


Résumé visuel : le flux complet

                    ┌───────────────────────────────────┐
                    │         target_*() appelé         │
                    │  (link_libraries, include_dirs,   │
                    │   compile_defs, compile_opts...)  │
                    └──────────────┬────────────────────┘
                                   │
                          ┌────────┴────────┐
                          │  Visibilité ?   │
                          └────────┬────────┘
                    ┌──────────────┼──────────────┐
                    ▼              ▼              ▼
               PRIVATE         PUBLIC        INTERFACE
                    │              │              │
                    ▼              ▼              ▼
            ┌─────────────┐ ┌──────────┐  ┌─────────────┐
            │ Build seul  │ │  Build   │  │ Usage seul  │
            │ (la cible)  │ │    +     │  │(consommat.) │
            │             │ │  Usage   │  │             │
            └─────────────┘ └──────────┘  └─────────────┘
                                │                │
                                └───────┬────────┘
                                        ▼
                              ┌────────────────────┐
                              │ Propagé via        │
                              │ target_link_libs() │
                              │ aux consommateurs  │
                              └────────────────────┘

Aide-mémoire final

Situation Visibilité Justification
Header public de la bibliothèque PUBLIC Le consommateur doit pouvoir l'inclure
Header d'implémentation interne PRIVATE Détail invisible pour le consommateur
Header d'une bibliothèque header-only INTERFACE Pas de build propre, tout est pour le consommateur
Dépendance dont les types sont dans l'API PUBLIC Le consommateur a besoin des mêmes types
Dépendance utilisée uniquement dans les .cpp PRIVATE Le consommateur n'a pas besoin de savoir
Flag de warning (-Wall, etc.) PRIVATE Les warnings sont une décision locale
Flag de sanitizer PRIVATE Le consommateur choisit ses propres sanitizers
Définition conditionnant un #ifdef dans un header public PUBLIC Le consommateur doit voir la même condition
Définition interne de tuning (BUFFER_SIZE, etc.) PRIVATE Paramètre d'implémentation
Standard C++ si les headers utilisent des features modernes PUBLIC Le consommateur doit compiler avec le même standard minimum
Standard C++ si seuls les .cpp utilisent des features modernes PRIVATE Le consommateur peut utiliser un standard différent

Le principe reste toujours le même : exposer le minimum nécessaire, encapsuler tout le reste. En cas de doute, PRIVATE. Le compilateur vous dira si c'est insuffisant — et à ce moment, vous remonterez en PUBLIC avec une justification claire.


À suivre : La section 26.3 aborde la gestion des dépendances externes — comment intégrer des bibliothèques tierces dans votre projet via find_package(), FetchContent, et add_subdirectory().

⏭️ Gestion des dépendances et sous-répertoires