1. Des flags globaux à l'approche target
Si vous n'avez pas travaillé auparavant sur de vrais projets, les « flags globaux » peuvent sembler séduisants : une seule commande set(...) — et tout devient mieux, plus propre et plus rapide. En pratique, cela ressemble souvent à vouloir piloter le chauffage d'un immeuble entier avec une seule vanne, alors que chaque locataire aime sa propre température. Le résultat est toujours le même : quelqu'un a trop chaud, quelqu'un a trop froid, et la faute revient (bien sûr) à CMake.
Avec CMake, les débutants sont souvent tentés d'écrire ceci :
# MAUVAIS (pour notre discipline d'approche target)
set(CMAKE_CXX_STANDARD 23)
add_definitions(-DDEBUG)
set(CMAKE_CXX_FLAGS "-Wall -Wextra")
include_directories(include)
Cela semble compact. Mais le problème est que ces réglages se « répandent » dans tout le projet et finissent par agir comme une magie invisible : vous n'êtes plus sûr de qui a reçu le flag -Wall, pourquoi un fichier a soudainement vu un en‑tête, ou pourquoi une cible se compile alors qu'une autre casse.
Pour sentir la douleur, imaginez que le projet contient :
- deux applications (par exemple, app et tool);
- une bibliothèque (par exemple, core);
- des tests (ils apparaitront plus tard dans le cours, mais l'idée est utile dès maintenant).
Et soudain il apparaît : l'application veut certains warnings et macros, la bibliothèque en veut d'autres, et les tests veulent vivre leur vie.
Avec l'approche target, on règle cela radicalement : on attache chaque configuration à une cible concrète. Le fichier CMake se lit alors non pas comme « un ensemble magique de sorts globaux », mais comme une description claire : voici la cible, voici ses sources, voici ses exigences et ses règles de compilation.
Qu'est‑ce qu'une target dans CMake
Le mot « target » dans CMake est d'abord perçu comme « le nom du binaire futur ». C'est partiellement vrai, mais trop simpliste. Il est plus utile de penser : une target est un objet avec des propriétés. Elle a un nom, une liste de sources, et tout un « dossier » de réglages : le standard du langage, les macros du préprocesseur, les options de compilation et plus tard — les chemins include et les dépendances de linkage.
Dans cette leçon nous retenons la règle principale :
Dans le style target, on évite d'écrire des « flags globaux pour tout le projet ».
On écrit des « propriétés pour une target spécifique ».
Autrement dit, au lieu de « définir tout pour tous quelque part en haut », on fait « voici la target app, et à côté sont listées clairement ses exigences ».
Syntaxiquement cela s'exprime par la famille de commandes target_*, par exemple :
- target_compile_features(...) — exigences sur les capacités du compilateur (y compris le standard),
- target_compile_definitions(...) — macros du préprocesseur,
- target_compile_options(...) — options du compilateur.
Aujourd'hui nous nous concentrons sur ces trois : elles montrent le mieux le sens de l'approche target sans nécessiter d'aborder tout de suite les include paths et le linkage.
2. Mini‑projet MiniCalc
Structure du projet
Pour rester concret, nous allons développer une petite application console. Elle s'appellera MiniCalc et sait additionner des nombres via une fonction séparée. L'application est simple, mais idéale pour expérimenter les réglages de compilation : le standard, les macros et les warnings.
La structure actuelle peut être :
MiniCalc/
CMakeLists.txt
src/
main.cpp
sum.cpp
sum.hpp
Remarquez : nous gardons pour l'instant sum.hpp à côté des .cpp dans le dossier src/, afin d'éviter de traiter maintenant la question des include‑directories. Déplacer les headers vers include/ est le sujet de la prochaine leçon.
Sources
Le code sera minimal.
src/sum.hpp :
#pragma once
int sum(int a, int b);
src/sum.cpp :
#include "sum.hpp"
int sum(int a, int b) {
return a + b;
}
src/main.cpp :
#include <iostream>
#include "sum.hpp"
int main() {
std::cout << "2 + 3 = " << sum(2, 3) << '\n'; // 2 + 3 = 5
return 0;
}
Nous commencerons la compilation avec un CMakeLists.txt minimal, puis nous l'améliorerons strictement en style target.
3. Configurer la cible via target_*
Standard du langage via target_compile_features
Quand le projet est petit, on a l'impression que le standard du langage peut être « défini une fois au sommet » et oublié. Mais c'est précisément le cas où le confort d'aujourd'hui devient l'énigme de demain. L'approche target propose un principe simple : le standard est une exigence de la cible, parce que c'est la cible qui est compilée, pas « le projet en général ».
Un CMakeLists.txt minimal en style target peut ressembler à ceci :
cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)
add_executable(app
src/main.cpp
src/sum.cpp
)
target_compile_features(app PRIVATE cxx_std_23)
Il est important d'expliquer immédiatement le sens des lignes sans « magie ».
add_executable(app ...) crée la target app. C'est notre futur exécutable.
target_compile_features(app PRIVATE cxx_std_23) signifie : « Pour compiler app, le compilateur doit supporter C++23 (et nous voulons que la compilation utilise ce standard). »
Le mot‑clé PRIVATE doit être compris pour l'instant comme « cela ne concerne que la target app ». Les subtilités de PUBLIC/INTERFACE deviennent particulièrement visibles pour les include‑dirs et les dépendances, mais nous nous habituons dès maintenant à l'idée : les exigences ont une visibilité, et non un simple on/off.
Pourquoi est‑ce mieux que set(CMAKE_CXX_STANDARD 23) ? Parce que vous voyez explicitement quelle cible exige C++23. Si un second target apparaît dans le projet, vous pourrez lui assigner un autre exigence de façon délibérée, et non par accident.
Macros de compilation via target_compile_definitions
Les macros du préprocesseur sont sournoises. Elles ressemblent à des « commutateurs de mode » du programme, mais en réalité ce sont des commutateurs de compilation, c'est‑à‑dire qu'elles changent quel code aboutira dans le binaire final. Donc stocker ces commutateurs « globalement pour tous » est presque toujours une mauvaise idée : vous rendez trop facile l'incompatibilité entre différentes parties du projet.
En approche target, on définit les macros ainsi :
target_compile_definitions(app PRIVATE APP_NAME="MiniCalc")
target_compile_definitions(app PRIVATE APP_VERSION=1)
Intégrons cela dans notre CMakeLists.txt :
cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)
add_executable(app
src/main.cpp
src/sum.cpp
)
target_compile_features(app PRIVATE cxx_std_23)
target_compile_definitions(app PRIVATE APP_NAME="MiniCalc")
target_compile_definitions(app PRIVATE APP_VERSION=1)
Utilisons maintenant ces macros dans main.cpp. Important : si une macro n'est pas définie, la compilation peut échouer. Donc prévoyons un « plan B » — des valeurs par défaut via #ifndef.
src/main.cpp :
#include <iostream>
#include "sum.hpp"
#ifndef APP_NAME
#define APP_NAME "(unknown)"
#endif
#ifndef APP_VERSION
#define APP_VERSION 0
#endif
int main() {
std::cout << APP_NAME << " v" << APP_VERSION << '\n'; // MiniCalc v1
std::cout << "2 + 3 = " << sum(2, 3) << '\n'; // 2 + 3 = 5
return 0;
}
Nous avons ici un bon « pont » entre la configuration de build et le code : CMake fournit des « paramètres de build » via target_compile_definitions, et le code C++ y répond proprement.
Une règle de discipline importante : ne transformez pas les macros en entrée utilisateur. Les macros ne remplacent pas std::cin. Elles servent à la configuration de compilation (par exemple, activer le logging debug, sélectionner un mode, incruster le nom du produit), mais pas pour « lire un nombre et le calculer ».
Options du compilateur via target_compile_options
Les warnings du compilateur sont comme les remarques d'un correcteur sur la marge : on peut les ignorer, mais on risque des problèmes plus tard. Dans les vrais projets, les warnings sont presque toujours activés, mais de manière réfléchie. Ici encore l'approche target gagne : les warnings sont une propriété d'une cible précise, pas d'une abstraction « build ».
La variante la plus directe :
target_compile_options(app PRIVATE -Wall -Wextra)
Mais nuance : les flags dépendent du compilateur. GCC/Clang comprennent -Wall, MSVC non (MSVC a ses propres /W4 et alliés). Nous n'entrons pas dans une portabilité complète, mais montrons une version minimale adéquate :
target_compile_options(app PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra>
)
Cela parait plus effrayant qu'en réalité. L'important maintenant n'est pas la syntaxe des « generator expressions », mais l'idée : les options sont collées à la target app.
Si vous préférez une version très simple sans generator expressions (en acceptant que MSVC se comporte différemment), vous pouvez commencer ainsi :
if (NOT MSVC)
target_compile_options(app PRIVATE -Wall -Wextra)
endif()
Au final notre CMakeLists.txt devient :
cmake_minimum_required(VERSION 3.20)
project(MiniCalc LANGUAGES CXX)
add_executable(app
src/main.cpp
src/sum.cpp
)
target_compile_features(app PRIVATE cxx_std_23)
target_compile_definitions(app PRIVATE APP_NAME="MiniCalc" APP_VERSION=1)
if (NOT MSVC)
target_compile_options(app PRIVATE -Wall -Wextra)
endif()
Remarquez le style caractéristique de l'approche target : on lit le fichier de haut en bas et l'on voit que app n'est pas juste un « binaire », mais une cible avec un ensemble clair d'exigences.
Où les réglages se collent dans l'approche target
Quand on entend pour la première fois « propriétés d'une target », on peut avoir l'impression que ce sont simplement des flags réécrits différemment. Pour que cela ne reste pas magique, il est utile de dessiner un schéma simple : la target est le centre, et les commandes target_* lui « ajoutent » des propriétés. Ainsi vous voyez littéralement ce qui se passe et vous cessez de percevoir CMake comme du chamanisme.
Voici un diagramme simplifié (sans includes ni linkage — sujets d'autres leçons) :
flowchart TD
T["target: app (executable)"]
S["sources: main.cpp, sum.cpp"] --> T
F["compile features: cxx_std_23"] --> T
D["compile definitions: APP_NAME, APP_VERSION"] --> T
O["compile options: -Wall, -Wextra"] --> T
Et voici l'effet « psychologique » principal de l'approche target : les réglages ne flottent plus « quelque part dans l'air », ils deviennent une partie de la description d'une cible spécifique.
Si plus tard une seconde target apparaît (par exemple, tool), vous ne vous demanderez pas « quels flags globaux sont actifs maintenant ? ». Vous ouvrirez simplement le CMakeLists.txt et verrez : tool est comme ceci, app est comme cela.
4. Erreurs typiques
Erreur n°1 : continuer à vivre en mode CMAKE_CXX_FLAGS et « magie globale ».
Il est très facile de commencer proprement (créer add_executable, écrire quelques target_*), puis de « rapidement ajouter » set(CMAKE_CXX_FLAGS "..."), parce que vous avez vu cela quelque part sur Internet. En général cela conduit à perdre la compréhension de pourquoi une target compile avec des warnings et une autre non, ou pourquoi de nouveaux flags cassent la build sur une autre machine.
Erreur n°2 : confondre target_compile_features, target_compile_definitions et target_compile_options.
Les débutants prennent parfois cela pour « trois façons de dire la même chose ». En réalité elles couvrent des domaines différents : features — exigences liées au langage/compilateur, definitions — macros du préprocesseur, options — flags du compilateur. Si on mélange tout, le CMakeLists.txt devient une « bourriche de flags » illisible.
Erreur n°3 : utiliser des macros de build comme substitution à l'entrée utilisateur.
Parfois on veut faire -DINPUT=5 et considérer que c'est un « paramètre du programme ». C'est un paramètre de compilation, pas d'exécution. Vous incorporez un nombre dans le binaire au lieu de le lire depuis la console. Cela peut se justifier pour la version/le nom/le mode de logging, mais c'est inadapté pour les « données utilisateur ».
Erreur n°4 : activer des options du compilateur « pour tous et pour toujours » sans comprendre le coût.
Même des flags utiles peuvent poser problème s'ils sont activés globalement : quelque part ils créeront un warning qui, pour une target, est tolérable, alors que pour une autre il casse la compilation. L'approche target enseigne justement à activer la qualité progressivement : des règles précises pour chaque cible.
Erreur n°5 : avoir honte d'un CMakeLists.txt long et tenter à tout prix de raccourcir les lignes.
Le débutant pense qu'un bon CMakeLists.txt doit être court. En pratique, un bon CMakeLists.txt doit être lisible. Si la lisibilité requiert quelques lignes supplémentaires près de add_executable(app ...), c'est normal. Le problème n'est pas « beaucoup de lignes », mais que les réglages soient cachés quelque part et incompréhensibles.
GO TO FULL VERSION