🔝 Retour au Sommaire
Chapitre 47 : Collaboration et Maintenance · Section 47.3 : Configuration pre-commit
Niveau : Expert
Prérequis : Section 47.3.1 (.pre-commit-config.yaml), section 32.3 (clang-format — formatage automatique)
La sous-section 47.3.1 a intégré clang-format dans le fichier .pre-commit-config.yaml avec quelques lignes de configuration. Mais derrière ces lignes se cache un outil d'une profondeur considérable : clang-format expose plus de 150 options de style, et un .clang-format mal conçu peut produire des résultats surprenants sur du code C++ moderne (concepts, ranges, structured bindings, lambdas imbriquées).
Cette sous-section couvre tout ce qu'il faut savoir pour que l'intégration de clang-format dans les pre-commit hooks soit robuste, déterministe et acceptée par l'équipe : la construction du fichier .clang-format, la gestion des cas particuliers, les mécanismes d'échappement, et les stratégies pour introduire le formatage sur un projet existant sans provoquer de révolte.
Le fichier .clang-format est un fichier YAML placé à la racine du projet. clang-format remonte l'arborescence à partir du fichier source pour trouver le .clang-format le plus proche. C'est pourquoi un seul fichier à la racine suffit dans la majorité des cas.
Plutôt que de configurer les 150+ options individuellement, clang-format propose des styles de base prédéfinis. Le choix du style de base est la décision la plus structurante :
| Style de base | Caractéristiques | Utilisé par |
|---|---|---|
LLVM |
Indentation 2, accolades attachées, colonnes 80 | Projets LLVM/Clang |
Google |
Indentation 2, accolades attachées, colonnes 80 | Google (C++, Java, etc.) |
Chromium |
Proche de Google, quelques variations | Projet Chromium |
Mozilla |
Indentation 2, accolades Allman pour fonctions | Firefox |
WebKit |
Indentation 4, accolades attachées | WebKit/Safari |
Microsoft |
Indentation 4, accolades Allman | Projets Microsoft |
GNU |
Indentation 2, accolades GNU (indentées) | Projets GNU |
Le style de base définit les valeurs par défaut de toutes les options. Vous ne surchargez ensuite que ce qui diffère de vos conventions :
# .clang-format
---
BasedOnStyle: LLVM
# ... surcharges ci-dessousRecommandation pragmatique : choisissez le style de base le plus proche de votre code existant. Un reformatage massif est mieux accepté s'il ne bouleverse que quelques aspects du style plutôt que l'intégralité. Si le projet n'a pas de style établi,
LLVMet
Voici un .clang-format complet, commenté, adapté au C++ moderne (C++20/23/26) et au travail en équipe. Il est conçu comme point de départ — chaque option est expliquée pour faciliter les ajustements :
# .clang-format
# ═══════════════════════════════════════════════════════════════
# Configuration clang-format — Projet C++ moderne (C++20/23/26)
# Requiert : clang-format 19+
# ═══════════════════════════════════════════════════════════════
---
Language: Cpp
BasedOnStyle: LLVM
# ── Indentation ──────────────────────────────────────────────
IndentWidth: 4
TabWidth: 4
UseTab: Never
IndentCaseLabels: true
IndentPPDirectives: BeforeHash
NamespaceIndentation: None
IndentRequiresClause: true # C++20 requires
IndentExternBlock: NoIndent
# ── Largeur et wrapping ─────────────────────────────────────
ColumnLimit: 100
ContinuationIndentWidth: 4
# ── Accolades ────────────────────────────────────────────────
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
# ── Espacement ───────────────────────────────────────────────
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpacesInAngles: Never
SpacesInParens: Never
# ── Alignement ───────────────────────────────────────────────
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: None
AlignConsecutiveDeclarations: None
AlignConsecutiveMacros: Consecutive
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments:
Kind: Always
OverEmptyLines: 1
# ── Arguments et paramètres ──────────────────────────────────
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
BinPackArguments: true
BinPackParameters: true
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
# ── Includes ─────────────────────────────────────────────────
SortIncludes: CaseSensitive
IncludeBlocks: Regroup
IncludeCategories:
# 1. Header associé (convention : même nom que le .cpp)
- Regex: '^"[^/]*\.h(pp)?"'
Priority: 1
SortPriority: 1
# 2. Headers du projet
- Regex: '^"'
Priority: 2
SortPriority: 2
# 3. Headers tiers (entre < >)
- Regex: '^<[a-z]'
Priority: 3
SortPriority: 3
# 4. Headers système / STL
- Regex: '^<'
Priority: 4
SortPriority: 4
# ── C++ moderne ──────────────────────────────────────────────
Standard: Latest
RequiresClausePosition: OwnLine # C++20 requires sur sa propre ligne
RequiresExpressionIndentation: OuterScope
BreakBeforeConceptDeclarations: Always
LambdaBodyIndentation: Signature
PackConstructorInitializers: NextLine
InsertBraces: false # Ne pas insérer automatiquement
InsertNewlineAtEOF: true
SeparateDefinitionBlocks: Leave
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
IntegerLiteralSeparator:
Binary: 4
Decimal: 3
Hex: 2
# ── Pénalités (contrôle du line-breaking) ────────────────────
PenaltyBreakAssignment: 25
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PenaltyBreakOpenParenthesis: 0
PenaltyBreakTemplateDeclaration: 10
# ── Divers ───────────────────────────────────────────────────
AccessModifierOffset: -4
CompactNamespaces: false
Cpp11BracedListStyle: true
DerivePointerAlignment: false
PointerAlignment: Left # int* ptr (pas int *ptr ni int * ptr)
ReferenceAlignment: Pointer
FixNamespaceComments: true
MaxEmptyLinesToKeep: 1
ReflowComments: true
QualifierAlignment: Leave # Ne pas réordonner const/volatile
RemoveSemicolon: false
...ColumnLimit: 100 — Le choix de la largeur de ligne est probablement la décision la plus débattue. 80 colonnes est le défaut historique ; 100-120 est devenu le standard de facto pour le C++ moderne, où les noms qualifiés (std::unordered_map<std::string, std::vector<int>>) et les signatures avec concepts consomment beaucoup de caractères. À 80 colonnes, le wrapping excessif nuit à la lisibilité. À 120+, les diffs côte à côte deviennent difficiles à lire. 100 est un compromis éprouvé.
PointerAlignment: Left — int* ptr plutôt que int *ptr. C'est le style prédominant en C++ moderne (cohérent avec la pensée "le type est int*"), même si le C et certains styles anciens préfèrent l'alignement à droite (cohérent avec la grammaire du langage). L'important est la cohérence — le choix importe moins que l'uniformité.
SortIncludes: CaseSensitive et IncludeCategories — Le tri automatique des #include est l'une des fonctionnalités les plus utiles de clang-format pour la collaboration. Sans tri, les includes s'accumulent dans un ordre aléatoire qui varie d'un développeur à l'autre, provoquant des conflits de merge artificiels. Le regroupement par catégories (header associé, projet, tiers, système) améliore la lisibilité et rend les dépendances explicites.
L'ordre header associé en premier est une convention de Google (et de nombreux projets) qui a un bénéfice technique : si connection_pool.h oublie un include nécessaire, connection_pool.cpp échouera à la compilation car le header est inclus avant tout autre. Cela force les headers à être self-contained.
RequiresClausePosition: OwnLine — Place les clauses requires C++20 sur leur propre ligne. C'est le style recommandé pour la lisibilité, surtout avec des contraintes complexes :
// Avec OwnLine
template<typename T>
requires std::integral<T> && std::is_signed_v<T>
auto process(T value) -> T;
// Sans (attaché) — difficile à lire avec des contraintes longues
template<typename T> requires std::integral<T> && std::is_signed_v<T>
auto process(T value) -> T; BreakBeforeConceptDeclarations: Always — Sépare visuellement les déclarations de concepts du code environnant, ce qui améliore la navigation dans les fichiers contenant plusieurs concepts.
IntegerLiteralSeparator — Active les séparateurs dans les littéraux numériques pour la lisibilité. clang-format 19+ peut insérer automatiquement des ' (digit separators C++14) dans les constantes : 1'000'000 plutôt que 1000000, 0xFF'FF plutôt que 0xFFFF. C'est une fonctionnalité optionnelle — retirez ce bloc si vous ne souhaitez pas de réécriture des littéraux.
QualifierAlignment: Leave — Ne réordonne pas les qualificateurs (const, volatile). Le débat const int vs int const (east const vs west const) est interminable. Leave respecte le choix du développeur. Si votre équipe a une convention, utilisez Left (west const : const int) ou Right (east const : int const).
Section Pénalités — Les pénalités contrôlent les décisions de clang-format quand une ligne dépasse la limite de colonnes. Chaque option assigne un "coût" à un type de coupure. clang-format choisit la coupure avec le coût total minimal. Les valeurs ci-dessus favorisent la coupure après les parenthèses ouvrantes et avant les déclarations de templates, tout en évitant de couper les chaînes de caractères et les commentaires. Ajuster ces pénalités est rarement nécessaire au début, mais c'est le levier pour corriger les cas de wrapping aberrant que vous rencontrerez à l'usage.
Le tri automatique des #include par clang-format est puissant mais nécessite de la vigilance dans certains cas.
Dans la grande majorité des fichiers, le tri des includes est sans effet secondaire et très bénéfique :
// Avant (ordre aléatoire, dépendances masquées)
#include <vector>
#include "config.h"
#include <string>
#include "network/connection_pool.h"
#include <memory>
#include "utils/logger.h"
// Après clang-format (regroupé et trié)
#include "network/connection_pool.h" // Header associé (priorité 1)
#include "config.h" // Headers projet (priorité 2)
#include "utils/logger.h"
#include <memory> // Headers STL (priorité 4)
#include <string>
#include <vector>Dans de rares cas, l'ordre des includes est sémantiquement significatif. Cela arrive quand un header dépend d'un #define défini avant l'include, ou quand un header pollue le namespace et doit être inclus après les autres :
// L'ordre est intentionnel ici — NE PAS TRIER
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG
#include <spdlog/spdlog.h>
// Ou dans du code legacy / interop C
#include <sys/types.h> // Doit être avant sys/socket.h sur certains systèmes
#include <sys/socket.h>Pour ces cas, clang-format fournit un mécanisme de protection :
// clang-format off
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG
#include <spdlog/spdlog.h>
// clang-format on
#include "my_module.h" // Le tri reprend normalement ici
#include <string>
#include <vector>Alternativement, un commentaire vide entre deux blocs d'includes empêche clang-format de les fusionner et de les trier ensemble :
#include <sys/types.h>
#include <sys/socket.h>
// ↑ Ce commentaire (ou une ligne vide) crée une séparation
// clang-format ne trie pas entre les deux groupes
#include <string>
#include <vector>Aucun outil de formatage n'est parfait. Il existe des cas où clang-format produit un résultat moins lisible que le formatage manuel. Les directives // clang-format off et // clang-format on permettent de désactiver ponctuellement le formatage :
Tableaux de données alignés manuellement — Le formatage tabulaire est souvent plus lisible que le wrapping automatique :
// clang-format off
constexpr std::array<KeyMapping, 6> key_mappings = {{
{ Key::Up, Action::MoveUp, "Move up" },
{ Key::Down, Action::MoveDown, "Move down" },
{ Key::Left, Action::MoveLeft, "Move left" },
{ Key::Right, Action::MoveRight, "Move right" },
{ Key::Space, Action::Jump, "Jump" },
{ Key::Escape, Action::OpenMenu, "Open menu" },
}};
// clang-format onSans les directives, clang-format casserait l'alignement des colonnes.
Macros préprocesseur complexes — clang-format ne comprend pas la sémantique des macros et peut mal formater les continuations \ :
// clang-format off
#define REGISTER_TEST(name, func, timeout) \
static auto test_##name = TestRegistry \
::instance() \
.add(#name, func, timeout)
// clang-format onDSL (Domain-Specific Languages) embarqués — Certaines bibliothèques utilisent des macros ou des operator overloads pour créer des mini-DSL dont le formatage intentionnel est significatif :
// clang-format off
auto pipeline = source
| views::filter([](auto x) { return x > 0; })
| views::transform([](auto x) { return x * 2; })
| views::take(10);
// clang-format onCet exemple est de moins en moins nécessaire avec les versions récentes de
clang-format(18+) qui gèrent mieux le formatage des pipelines ranges C++20.
L'abus de // clang-format off est un signal d'alarme. Si plus de 5 % des fichiers contiennent des désactivations, c'est un indicateur que le .clang-format est mal configuré ou que le style choisi est trop éloigné des conventions naturelles de l'équipe. Ajustez la configuration plutôt que de multiplier les échappements.
Quelques principes pour une utilisation saine :
- Toujours refermer : chaque
// clang-format offdoit avoir son// clang-format on. Unoffsansondésactive le formatage jusqu'à la fin du fichier. - Scope minimal : n'encadrez que les lignes qui en ont besoin, pas des fonctions entières.
- Documenter la raison : un commentaire sur la même ligne que le
offaide les futurs lecteurs :
// clang-format off — alignement tabulaire intentionnel
constexpr auto table = ...;
// clang-format onLe C++ moderne utilise de plus en plus d'attributs ([[nodiscard]], [[maybe_unused]], [[deprecated("...")]]). clang-format les gère bien dans la plupart des cas, mais quelques options méritent attention :
AttributeMacros — Si votre projet définit des macros qui se comportent comme des attributs (annotation de visibilité, export DLL, instrumentation), déclarez-les pour que clang-format les traite correctement :
# Dans .clang-format
AttributeMacros:
- MY_EXPORT
- MY_DEPRECATED
- BENCHMARK_ATTRSans cette déclaration, clang-format peut confondre ces macros avec des appels de fonction et insérer des espaces ou des retours à la ligne inappropriés.
StatementAttributeLikeMacros et TypenameMacros — Même logique pour les macros qui simulent des statements ou des noms de type :
StatementAttributeLikeMacros:
- Q_EMIT
- Q_SIGNAL
- Q_SLOT
TypenameMacros:
- STACK_OF
- LIST_ENTRYCes déclarations sont surtout nécessaires dans les projets utilisant Qt, OpenSSL ou d'autres bibliothèques C à forte densité de macros.
Le fichier .clang-format est utilisé non seulement par le hook pre-commit, mais aussi par l'éditeur en temps réel. La plupart des IDE et éditeurs modernes appliquent clang-format au moment de la sauvegarde ou du formatage manuel.
L'extension C/C++ (ms-vscode.cpptools) ou clangd utilise automatiquement le .clang-format du projet. Activez le formatage à la sauvegarde dans les settings du projet :
Avec cette configuration, le développeur n'a même plus besoin du hook pre-commit pour le formatage — le fichier est formaté à chaque sauvegarde. Le hook pre-commit reste utile comme filet de sécurité (l'éditeur peut être mal configuré, ou le développeur peut utiliser un autre éditeur).
CLion intègre clang-format nativement. Activez-le dans Settings → Editor → Code Style → C/C++ → Enable ClangFormat. CLion détecte automatiquement le .clang-format à la racine du projet.
Avec le plugin vim-clang-format ou via clangd (LSP natif) :
" .vimrc ou init.vim
let g:clang_format#detect_style_file = 1
autocmd BufWritePre *.cpp,*.h,*.hpp :ClangFormat Quand l'éditeur formate à la sauvegarde et que le hook pre-commit vérifie le formatage, le développeur n'est pratiquement jamais bloqué par le hook. Le formatage devient invisible — il se produit automatiquement, en continu, sans intervention consciente. C'est l'état souhaité : la conformité de style ne coûte zéro effort mental.
En CI, on ne veut pas que clang-format modifie les fichiers — on veut qu'il vérifie que le formatage est correct et échoue si ce n'est pas le cas. L'approche diffère selon la méthode :
# CI pipeline
pre-commit run clang-format --all-filesLe hook pre-commit utilise -i (in-place) par défaut. En CI, cela signifie que si un fichier est mal formaté, le hook le modifie et retourne un code d'erreur. Le pipeline échoue, ce qui est le comportement souhaité. Les fichiers modifiés ne sont pas commités (l'environnement CI est éphémère).
Pour une vérification sans modification dans un script CI personnalisé :
# Vérifie que tous les fichiers sont correctement formatés
# --dry-run : n'écrit pas les modifications
# --Werror : retourne un code d'erreur si une modification serait nécessaire
find src/ include/ -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \
| xargs clang-format --dry-run --Werror --style=filePour aider le développeur à comprendre ce qui doit être corrigé :
# Affiche le diff entre le fichier actuel et le fichier formaté
find src/ include/ -name '*.cpp' -o -name '*.hpp' -o -name '*.h' \
| xargs -I{} bash -c 'diff <(cat "{}") <(clang-format --style=file "{}") && true || echo "→ {}"'Quand une nouvelle version de clang-format introduit une option utile, ajoutez-la au .clang-format dans un commit dédié :
# 1. Ajouter l'option
vim .clang-format
# 2. Reformater tout le projet avec la nouvelle option
clang-format -i $(find src/ include/ -name '*.cpp' -o -name '*.hpp' -o -name '*.h')
# 3. Commiter le changement de config + le reformatage ensemble
git add .clang-format src/ include/
git commit -m "style: enable IntegerLiteralSeparator in clang-format
Adds digit separators to integer literals for readability.
Requires clang-format 19+."
# 4. Ajouter ce commit à .git-blame-ignore-revs
echo "" >> .git-blame-ignore-revs
echo "# Enable IntegerLiteralSeparator" >> .git-blame-ignore-revs
echo "$(git rev-parse HEAD)" >> .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -m "chore: update git-blame-ignore-revs" Quand l'équipe passe d'une version majeure à une autre (par exemple de 18 à 19), le formatage de certains fichiers peut changer même sans modification du .clang-format. Procédez de la même manière : un commit de reformatage dédié + ajout à .git-blame-ignore-revs.
Testez toujours la nouvelle version sur l'ensemble du projet avant de la déployer :
# Comparer le formatage entre deux versions
clang-format-18 --style=file src/core/engine.cpp > /tmp/v18.cpp
clang-format-19 --style=file src/core/engine.cpp > /tmp/v19.cpp
diff /tmp/v18.cpp /tmp/v19.cpp Si les différences sont acceptables, procédez à la migration. Si certains changements sont indésirables, ajoutez des options explicites au .clang-format pour préserver le comportement précédent (les nouvelles versions ajoutent des options, elles ne suppriment pas les anciennes).
Si votre projet a un style existant non formalisé, clang-format peut générer un .clang-format qui s'en approche :
# Demander à clang-format de deviner le style d'un fichier
clang-format --dump-config --style=file src/core/engine.cpp > .clang-format.draftSi aucun .clang-format n'existe encore, la commande suivante génère un fichier basé sur un style prédéfini :
# Générer le .clang-format pour le style Google
clang-format --dump-config --style=Google > .clang-formatCela produit un fichier exhaustif avec toutes les options explicitées. Vous pouvez ensuite le simplifier en ne gardant que les options qui diffèrent du BasedOnStyle choisi.
Un outil interactif utile est le Clang-Format Configurator — une interface web qui permet de prévisualiser l'effet de chaque option sur un extrait de code. C'est particulièrement utile pour les options de pénalité et de wrapping, dont l'effet n'est pas toujours intuitif à partir de la documentation seule.
La version de clang-format installée est trop ancienne pour l'option utilisée. Chaque option a une version minimale requise :
| Option | Version minimale |
|---|---|
RequiresClausePosition |
15 |
RequiresExpressionIndentation |
16 |
InsertNewlineAtEOF |
16 |
BreakBeforeConceptDeclarations |
12 |
IntegerLiteralSeparator |
16 |
AlignTrailingComments (struct) |
16 |
Solution : mettez à jour clang-format ou retirez les options non supportées. Le hook de vérification de version présenté en section 47.3 détecte ce problème avant qu'il ne se manifeste.
Deux développeurs obtiennent un formatage différent sur le même fichier. Causes possibles :
- Versions différentes de
clang-format— Vérifiez avecclang-format --version. C'est la cause la plus fréquente. - Plusieurs
.clang-formatdans l'arborescence — Un.clang-formatdans un sous-répertoire prend la priorité sur celui de la racine. Vérifiez qu'il n'y en a pas d'autre. .clang-formatnon versionné — Un développeur a un.clang-formatlocal modifié qu'il n'a pas commité.
Un fichier est reformaté à chaque commit, même si rien d'autre n'a changé. Cause habituelle : une option de clang-format produit un résultat non idempotent sur certaines constructions syntaxiques (rare mais possible, surtout avec les macros complexes). Solution : isolez le code concerné avec // clang-format off et signalez le bug upstream si applicable.
Un #include a été déplacé par le tri et le fichier ne compile plus. Cause : dépendance d'ordre non explicite entre les includes (un header qui nécessite un #define préalable ou un autre include). Solution : utilisez // clang-format off autour du bloc d'includes concerné et commentez la dépendance.
L'intégration de clang-format dans les pre-commit hooks repose sur trois piliers : un fichier .clang-format bien conçu et versionné, une version d'outil homogène dans l'équipe, et un formatage à la sauvegarde dans l'éditeur qui rend le hook quasi transparent. Le .clang-format proposé dans cette section couvre les constructions C++ modernes (concepts, requires, ranges, structured bindings) et constitue un point de départ solide. Adaptez-le aux conventions de votre équipe, et ajoutez chaque commit de reformatage à .git-blame-ignore-revs pour préserver un historique Git exploitable.