🔝 Retour au Sommaire
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.
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.
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 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 |
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 foundationNET_BUFFER_SIZE=8192— PRIVATE dans networkOpenSSL::SSLet 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.oEt en flags de linkage :
g++ main.o -lnetwork -lfoundation -lssl -lcrypto -lpthread -o serverNotez 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.
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.
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) 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 directoryLa 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'APItarget_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.
# ❌ 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.
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.
Le type de la bibliothèque contraint les visibilités valides :
Les trois visibilités sont disponibles. C'est le cas standard couvert tout au long de cette section.
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.
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) 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.
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.
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) 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.
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.
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.
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.
┌───────────────────────────────────┐
│ 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 │
└────────────────────┘
| 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, etadd_subdirectory().