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é des types, la sécurité des ressources, l'immutabilité et la clarté.
Quand les utiliser
- Écrire du nouveau code C++ (classes, fonctions, templates)
- Réviser ou remanier 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 (ex. :
enum vs enum class, pointeur brut vs pointeur intelligent)
Quand ne PAS les utiliser
- Projets non C++
- Bases de code C legacy qui ne peuvent pas adopter les fonctionnalités C++ modernes
- Contextes embarqués / bare-metal où certaines règles entrent en conflit avec des 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é des types (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, types et 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 plutôt que de pointeur (C.10, R.3-5, F.20, CP.31) : Privilégier le retour par valeur et les objets de 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 type-safe |
| 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 brut ou une référence |
| I.23 | Garder le nombre d'arguments de fonction faible |
À FAIRE
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
À NE PAS FAIRE
double boil(double* temp);
int g_counter = 0;
Fonctions (F.*)
Règles clés
| Règle | Résumé |
|---|
| F.1 | Empaqueter 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 une 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 retourner plusieurs valeurs "de sortie", préférer retourner une structure |
| F.43 | Ne jamais retourner un pointeur ou une référence vers un objet local |
Passage de paramètres
void print(int x);
void analyze(const std::string& data);
void transform(std::string s);
struct ParseResult {
std::string token;
int position;
};
ParseResult parse(std::string_view input);
void parse(std::string_view input,
std::string& token, int& pos);
Fonctions pures et constexpr
constexpr int factorial(int n) noexcept {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
static_assert(factorial(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 membres de données varient indépendamment |
| C.9 | Minimiser l'exposition des membres |
| C.20 | Si vous pouvez éviter de définir les opérations spéciales, faites-le (Règle de Zéro) |
| C.21 | Si vous définissez ou =delete un constructeur de copie/déplacement ou 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 polymorphique 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 Employee {
std::string name;
std::string department;
int id;
};
Règle de Cinq
class Buffer {
public:
explicit Buffer(std::size_t size)
: data_(std::make_unique<char[]>(size)), size_(size) {}
~Buffer() = default;
Buffer(const Buffer& other)
: data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
std::copy_n(other.data_.get(), size_, data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
auto new_data = std::make_unique<char[]>(other.size_);
std::copy_n(other.data_.get(), other.size_, new_data.get());
data_ = std::move(new_data);
size_ = other.size_;
}
return *this;
}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
private:
std::unique_ptr<char[]> data_;
std::size_t size_;
};
Hiérarchie de classes
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
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 remplacement (C.140)
- Rendre les membres de données
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 brut (T*) n'est pas propriétaire |
| R.5 | Préférer les objets de 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 si la propriété est 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 render(const Widget* w) {
if (w) w->draw();
}
render(widget.get());
Patron RAII
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: handle_(std::fopen(path.c_str(), "r")) {
if (!handle_) throw std::runtime_error("Échec d'ouverture : " + path);
}
~FileHandle() {
if (handle_) std::fclose(handle_);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(other.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 -- risque de sécurité d'exception)
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 intentionnelle |
| 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 restrictives/avec perte |
| ES.47 | Utiliser nullptr plutôt que 0 ou NULL |
| ES.48 | Éviter les casts |
| ES.50 | Ne pas enlever const avec un cast |
Initialisation
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};
const auto config = [&] {
Config c;
c.timeout = std::chrono::seconds{30};
c.retries = max_retries;
c.verbose = debug_mode;
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 avec un cast (ES.50)
- Nombres magiques sans constantes nommées (ES.45)
- Mélanger l'arithmétique signée et non signée (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 la conception |
| E.2 | Lever une exception pour signaler qu'une fonction ne peut pas effectuer sa tâche assignée |
| E.6 | Utiliser RAII pour éviter les fuites |
| E.12 | Utiliser noexcept lorsque lever est impossible ou inacceptable |
| E.14 | Utiliser des types définis par l'utilisateur conçus pour cela 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 toutes les exceptions dans chaque fonction |
Hiérarchie d'exceptions
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class NetworkError : public AppError {
public:
NetworkError(const std::string& msg, int code)
: AppError(msg), status_code(code) {}
int status_code;
};
void fetch_data(const std::string& url) {
throw NetworkError("connexion refusée", 503);
}
void run() {
try {
fetch_data("https://api.example.com");
} catch (const NetworkError& e) {
log_error(e.what(), e.status_code);
} catch (const AppError& e) {
log_error(e.what());
}
}
Anti-patrons
- Lever des types intégrés comme
int ou des chaînes littérales (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 à 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 Sensor {
public:
explicit Sensor(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double last_reading() const { return reading_; }
void record(double value) { reading_ = value; }
private:
const std::string id_;
double reading_{0.0};
};
void display(const Sensor& s) {
std::cout << s.id() << ": " << s.last_reading() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;
Concurrence et parallélisme (CP.*)
Règles clés
| Règle | Résumé |
|---|
| CP.2 | Éviter les courses de données |
| CP.3 | Minimiser le partage explicite de données inscriptibles |
| 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 | Se souvenir de nommer vos lock_guard et unique_lock |
| CP.100 | Ne pas utiliser la programmation sans verrou sauf si absolument nécessaire |
Verrouillage sécurisé
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
queue_.push(value);
cond_.notify_one();
}
private:
std::queue<int> queue_;
std::mutex mutex_;
std::condition_variable cond_;
};
Communication thread-safe
std::mutex mtx;
std::condition_variable cv;
bool ready{false};
void worker() {
std::unique_lock lock(mtx);
cv.wait(lock, [] { return ready; });
}
void signal() {
{
std::scoped_lock lock(mtx);
ready = true;
}
cv.notify_one();
}
Anti-patrons
volatile pour la synchronisation (CP.8)
- Utiliser
lock()/unlock() sans RAII (CP.20)
- Attendre en boucle sans variable de condition (CP.42)
- Verrous non nommés (CP.44)
- Programmation sans verrou sans compréhension approfondie (CP.100)
- Passage de données entre threads avec des courses de données non intentionnelles (CP.2)
Nuages et attendant
Anti-patrons
- Utiliser
volatile pour la synchronisation (CP.8)
- Utiliser
lock()/unlock() sans RAII (CP.20)
- Attendre en boucle sans variable de condition (CP.42)
- Verrous non nommés (CP.44)
- Programmation sans verrou sans compréhension approfondie (CP.100)
- Passage de données entre threads avec des courses de données non intentionnelles (CP.2)