Normes de codage C++ (C++ Core Guidelines)
Normes de codage complètes pour le C++ moderne (C++17/20/23) dérivées des C++ Core Guidelines. Applique la sécurité de type, la sécurité des ressources, l'immutabilité et la clarté.
Quand l'utiliser
- Écrire du nouveau code C++ (classes, fonctions, templates)
- Relire ou refactorer du code C++ existant
- Prendre des décisions architecturales dans des projets C++
- Appliquer un style cohérent dans une base de code C++
- Choisir entre des fonctionnalités du langage (par exemple,
enum vs enum class, pointeur nu vs pointeur intelligent)
Quand NE PAS l'utiliser
- Projets non C++
- Bases de code C héritées qui ne peuvent pas adopter les fonctionnalités C++ modernes
- Contextes embarqués/bare-metal où certaines directives entrent en conflit avec les contraintes matérielles (à adapter sélectivement)
Principes transversaux
Ces thèmes reviennent dans l'ensemble des directives et en constituent le fondement :
- RAII partout (P.8, R.1, E.6, CP.20) : Lier la durée de vie des ressources à celle des objets
- Immutabilité par défaut (P.10, Con.1-5, ES.25) : Commencer par
const/constexpr ; la mutabilité est l'exception
- Sécurité de type (P.4, I.4, ES.46-49, Enum.3) : Utiliser le système de types pour prévenir les erreurs à la compilation
- Exprimer l'intention (P.3, F.1, NL.1-2, T.10) : Les noms, les types et les concepts doivent communiquer le but
- Minimiser la complexité (F.2-3, ES.5, Per.4-5) : Un code simple est un code correct
- Sémantique de valeur vs sémantique de pointeur (C.10, R.3-5, F.20, CP.31) : Préférer le retour par valeur et les objets à portée
Philosophie et interfaces (P., I.)
Règles clés
| Règle | Résumé |
|---|
| P.1 | Exprimer les idées directement dans le code |
| P.3 | Exprimer l'intention |
| P.4 | Idéalement, un programme devrait être statiquement typé |
| P.5 | Préférer la vérification à la compilation à la vérification à l'exécution |
| P.8 | Ne pas fuir de ressources |
| P.10 | Préférer les données immuables aux données mutables |
| I.1 | Rendre les interfaces explicites |
| I.2 | Éviter les variables globales non const |
| I.4 | Rendre les interfaces précisément et fortement typées |
| I.11 | Ne jamais transférer la propriété par un pointeur nu ou une référence |
| I.23 | Garder le nombre d'arguments de fonction bas |
À FAIRE
struct Temperature {
double kelvin;
};
Temperature bouillir(const Temperature& eau);
À NE PAS FAIRE
double bouillir(double* temp);
int g_compteur = 0;
Fonctions (F.*)
Règles clés
| Règle | Résumé |
|---|
| F.1 | Encapsuler les opérations significatives dans des fonctions soigneusement nommées |
| F.2 | Une fonction doit effectuer une seule opération logique |
| F.3 | Garder les fonctions courtes et simples |
| F.4 | Si une fonction peut être évaluée à la compilation, la déclarer constexpr |
| F.6 | Si votre fonction ne doit pas lever d'exception, la déclarer noexcept |
| F.8 | Préférer les fonctions pures |
| F.16 | Pour les paramètres "d'entrée", passer les types peu coûteux à copier par valeur et les autres par const& |
| F.20 | Pour les valeurs "de sortie", préférer les valeurs de retour aux paramètres de sortie |
| F.21 | Pour renvoyer plusieurs valeurs "de sortie", préférer renvoyer une structure |
| F.43 | Ne jamais retourner un pointeur ou une référence vers un objet local |
Passage de paramètres
void afficher(int x);
void analyser(const std::string& donnees);
void transformer(std::string s);
struct ResultatParsing {
std::string jeton;
int position;
};
ResultatParsing parser(std::string_view entree);
void parser(std::string_view entree,
std::string& jeton, int& pos);
Fonctions pures et constexpr
constexpr int factorielle(int n) noexcept {
return (n <= 1) ? 1 : n * factorielle(n - 1);
}
static_assert(factorielle(5) == 120);
Anti-patrons
- Retourner
T&& depuis des fonctions (F.45)
- Utiliser
va_arg / les variadiques à la C (F.55)
- Capturer par référence dans des lambdas passées à d'autres threads (F.53)
- Retourner
const T qui inhibe la sémantique de déplacement (F.49)
Classes et hiérarchies de classes (C.*)
Règles clés
| Règle | Résumé |
|---|
| C.2 | Utiliser class si un invariant existe ; struct si les données membres varient indépendamment |
| C.9 | Minimiser l'exposition des membres |
| C.20 | Si vous pouvez éviter de définir les opérations par défaut, faites-le (règle de zéro) |
| C.21 | Si vous définissez ou =delete une copie/déplacement/destructeur, gérez-les tous (règle de cinq) |
| C.35 | Destructeur de classe de base : public virtual ou protected non-virtual |
| C.41 | Un constructeur doit créer un objet complètement initialisé |
| C.46 | Déclarer les constructeurs à un seul argument explicit |
| C.67 | Une classe polymorphe doit supprimer la copie/déplacement publics |
| C.128 | Fonctions virtuelles : spécifier exactement un de virtual, override ou final |
Règle de zéro
struct Employe {
std::string nom;
std::string departement;
int id;
};
Règle de cinq
class Tampon {
public:
explicit Tampon(std::size_t taille)
: donnees_(std::make_unique<char[]>(taille)), taille_(taille) {}
~Tampon() = default;
Tampon(const Tampon& autre)
: donnees_(std::make_unique<char[]>(autre.taille_)), taille_(autre.taille_) {
std::copy_n(autre.donnees_.get(), taille_, donnees_.get());
}
Tampon& operator=(const Tampon& autre) {
if (this != &autre) {
auto nouvelles_donnees = std::make_unique<char[]>(autre.taille_);
std::copy_n(autre.donnees_.get(), autre.taille_, nouvelles_donnees.get());
donnees_ = std::move(nouvelles_donnees);
taille_ = autre.taille_;
}
return *this;
}
Tampon(Tampon&&) noexcept = default;
Tampon& operator=(Tampon&&) noexcept = default;
private:
std::unique_ptr<char[]> donnees_;
std::size_t taille_;
};
Hiérarchie de classes
class Forme {
public:
virtual ~Forme() = default;
virtual double aire() const = 0;
};
class Cercle : public Forme {
public:
explicit Cercle(double r) : rayon_(r) {}
double aire() const override { return 3.14159 * rayon_ * rayon_; }
private:
double rayon_;
};
Anti-patrons
- Appeler des fonctions virtuelles dans les constructeurs/destructeurs (C.82)
- Utiliser
memset/memcpy sur des types non triviaux (C.90)
- Fournir des arguments par défaut différents pour une fonction virtuelle et son override (C.140)
- Déclarer les données membres
const ou des références, ce qui supprime le déplacement/la copie (C.12)
Gestion des ressources (R.*)
Règles clés
| Règle | Résumé |
|---|
| R.1 | Gérer les ressources automatiquement en utilisant RAII |
| R.3 | Un pointeur nu (T*) n'est pas propriétaire |
| R.5 | Préférer les objets à portée ; ne pas allouer sur le tas inutilement |
| R.10 | Éviter malloc()/free() |
| R.11 | Éviter d'appeler new et delete explicitement |
| R.20 | Utiliser unique_ptr ou shared_ptr pour représenter la propriété |
| R.21 | Préférer unique_ptr à shared_ptr sauf en cas de propriété partagée |
| R.22 | Utiliser make_shared() pour créer des shared_ptr |
Utilisation des pointeurs intelligents
auto widget = std::make_unique<Widget>("config");
auto cache = std::make_shared<Cache>(1024);
void rendre(const Widget* w) {
if (w) w->draw();
}
rendre(widget.get());
Patron RAII
class GestionnaireFichier {
public:
explicit GestionnaireFichier(const std::string& chemin)
: handle_(std::fopen(chemin.c_str(), "r")) {
if (!handle_) throw std::runtime_error("Échec d'ouverture : " + chemin);
}
~GestionnaireFichier() {
if (handle_) std::fclose(handle_);
}
GestionnaireFichier(const GestionnaireFichier&) = delete;
GestionnaireFichier& operator=(const GestionnaireFichier&) = delete;
GestionnaireFichier(GestionnaireFichier&& autre) noexcept
: handle_(std::exchange(autre.handle_, nullptr)) {}
GestionnaireFichier& operator=(GestionnaireFichier&& autre) noexcept {
if (this != &autre) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(autre.handle_, nullptr);
}
return *this;
}
private:
std::FILE* handle_;
};
Anti-patrons
new/delete nus (R.11)
malloc()/free() dans du code C++ (R.10)
- Allocations multiples de ressources dans une seule expression (R.13 -- danger de sécurité des exceptions)
shared_ptr là où unique_ptr suffit (R.21)
Expressions et instructions (ES.*)
Règles clés
| Règle | Résumé |
|---|
| ES.5 | Garder les portées petites |
| ES.20 | Toujours initialiser un objet |
| ES.23 | Préférer la syntaxe d'initialisation {} |
| ES.25 | Déclarer les objets const ou constexpr sauf si la modification est prévue |
| ES.28 | Utiliser des lambdas pour l'initialisation complexe de variables const |
| ES.45 | Éviter les constantes magiques ; utiliser des constantes symboliques |
| ES.46 | Éviter les conversions arithmétiques réductrices/imprécises |
| ES.47 | Utiliser nullptr plutôt que 0 ou NULL |
| ES.48 | Éviter les casts |
| ES.50 | Ne pas enlever const |
Initialisation
const int tentatives_max{3};
const std::string nom{"widget"};
const std::vector<int> premiers{2, 3, 5, 7, 11};
const auto config = [&] {
Config c;
c.timeout = std::chrono::seconds{30};
c.tentatives = tentatives_max;
c.verbose = mode_debug;
return c;
}();
Anti-patrons
- Variables non initialisées (ES.20)
- Utiliser
0 ou NULL comme pointeur (ES.47 -- utiliser nullptr)
- Casts à la C (ES.48 -- utiliser
static_cast, const_cast, etc.)
- Enlever
const (ES.50)
- Nombres magiques sans constantes nommées (ES.45)
- Mélanger des opérations arithmétiques signées et non signées (ES.100)
- Réutiliser des noms dans des portées imbriquées (ES.12)
Gestion des erreurs (E.*)
Règles clés
| Règle | Résumé |
|---|
| E.1 | Développer une stratégie de gestion des erreurs tôt dans une conception |
| E.2 | Lever une exception pour signaler qu'une fonction ne peut pas accomplir sa tâche assignée |
| E.6 | Utiliser RAII pour prévenir les fuites |
| E.12 | Utiliser noexcept lorsque lever est impossible ou inacceptable |
| E.14 | Utiliser des types définis par l'utilisateur conçus à cet effet comme exceptions |
| E.15 | Lever par valeur, attraper par référence |
| E.16 | Les destructeurs, la désallocation et le swap ne doivent jamais échouer |
| E.17 | Ne pas essayer d'attraper chaque exception dans chaque fonction |
Hiérarchie des exceptions
class ErreurApp : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class ErreurReseau : public ErreurApp {
public:
ErreurReseau(const std::string& msg, int code)
: ErreurApp(msg), code_statut(code) {}
int code_statut;
};
void fetch_donnees(const std::string& url) {
throw ErreurReseau("connexion refusée", 503);
}
void executer() {
try {
fetch_donnees("https://api.example.com");
} catch (const ErreurReseau& e) {
log_erreur(e.what(), e.code_statut);
} catch (const ErreurApp& e) {
log_erreur(e.what());
}
}
Anti-patrons
- Lever des types intégrés comme
int ou des littéraux de chaîne (E.14)
- Attraper par valeur (risque de slicing) (E.15)
- Blocs catch vides qui avalent silencieusement les erreurs
- Utiliser les exceptions pour le contrôle de flux (E.3)
- Gestion des erreurs basée sur un état global comme
errno (E.28)
Constantes et immutabilité (Con.*)
Toutes les règles
| Règle | Résumé |
|---|
| Con.1 | Par défaut, rendre les objets immuables |
| Con.2 | Par défaut, rendre les fonctions membres const |
| Con.3 | Par défaut, passer les pointeurs et références en const |
| Con.4 | Utiliser const pour les valeurs qui ne changent pas après la construction |
| Con.5 | Utiliser constexpr pour les valeurs calculables à la compilation |
class Capteur {
public:
explicit Capteur(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double derniere_lecture() const { return lecture_; }
void enregistrer(double valeur) { lecture_ = valeur; }
private:
const std::string id_;
double lecture_{0.0};
};
void afficher(const Capteur& s) {
std::cout << s.id() << ": " << s.derniere_lecture() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_CAPTEURS = 256;
Concurrence et parallélisme (CP.*)
Règles clés
| Règle | Résumé |
|---|
| CP.2 | Éviter les courses aux données |
| CP.3 | Minimiser le partage explicite de données modifiables |
| CP.4 | Penser en termes de tâches, plutôt que de threads |
| CP.8 | Ne pas utiliser volatile pour la synchronisation |
| CP.20 | Utiliser RAII, jamais de lock()/unlock() nus |
| CP.21 | Utiliser std::scoped_lock pour acquérir plusieurs mutex |
| CP.22 | Ne jamais appeler du code inconnu en détenant un verrou |
| CP.42 | Ne pas attendre sans condition |
| CP.44 | Ne pas oublier de nommer vos lock_guard et unique_lock |
| CP.100 | Ne pas utiliser la programmation sans verrou sauf en cas d'absolue nécessité |
Verrouillage sûr
class FileAttenteThreadSafe {
public:
void push(int valeur
...