C++ Core Guidelines et patrons professionnels
Découvrez les C++ Core Guidelines, les conventions de nommage, la const-correction, les principes de conception d'API, les pratiques de documentation, le profilage des performances et les techniques de débogage en production.
Écrire du code C++ professionnel
Écrire du C++ correct et efficace est nécessaire, mais pas suffisant pour un développement logiciel professionnel. Un C++ professionnel, c'est aussi du code lisible (compréhensible par les autres et par vous-même dans six mois), maintenable (facile à modifier sans introduire de bugs) et robuste (qui gère les erreurs proprement et échoue de façon prévisible). Les C++ Core Guidelines, maintenues par Bjarne Stroustrup et Herb Sutter, codifient des décennies d'expérience durement acquise en règles concrètes. Cette leçon couvre les règles et pratiques professionnelles qui ont le plus d'impact.
Vue d'ensemble des C++ Core Guidelines
Les Core Guidelines couvrent la gestion des ressources, les interfaces, la gestion des erreurs et le nommage. Voici quelques-unes des règles les plus importantes en pratique :
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <span>
#include <stdexcept>
// R.1: Manage resources automatically using RAII
class DatabaseConnection {
public:
explicit DatabaseConnection(const std::string& connection_string) {
// Acquire resource in constructor
std::cout << "Connected to: " << connection_string << "\n";
}
~DatabaseConnection() {
// Release resource in destructor — always runs
std::cout << "Disconnected\n";
}
// Rule of 5: if you define a destructor, define or delete all 5
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
DatabaseConnection(DatabaseConnection&&) noexcept = default;
DatabaseConnection& operator=(DatabaseConnection&&) noexcept = default;
void query(std::string_view sql) const {
std::cout << "Executing: " << sql << "\n";
}
};
// I.4: Make interfaces precisely and strongly typed
// BAD: void process(int type, void* data, int size);
// GOOD:
enum class DataType { json, xml, csv };
void process(DataType type, std::span<const std::byte> data) {
// type-safe, no raw pointers, size is carried with the data
}
// F.3: Keep functions short and simple
[[nodiscard]] bool is_valid_email(std::string_view email) {
auto at_pos = email.find('@');
if (at_pos == std::string_view::npos) return false;
auto dot_pos = email.find('.', at_pos);
return dot_pos != std::string_view::npos && dot_pos > at_pos + 1;
}
int main() {
// R.5: Prefer scoped objects — no unnecessary heap allocation
DatabaseConnection db("host=localhost dbname=mydb");
db.query("SELECT * FROM users");
// db is automatically closed when it goes out of scope
// R.11: Avoid raw new/delete — use smart pointers
auto widget = std::make_unique<std::vector<int>>(100);
std::cout << is_valid_email("user@example.com") << "\n"; // 1
std::cout << is_valid_email("invalid-email") << "\n"; // 0
return 0;
}La const-correction comme discipline
La const-correction ne sert pas seulement à empêcher les modifications accidentelles — elle documente l'intention, permet des optimisations du compilateur et rend le code thread-safe par défaut. Rendez tout const, sauf ce qui doit être modifiable :
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
class StudentRegistry {
public:
// const method — promises not to modify the object
[[nodiscard]] size_t count() const { return students_.size(); }
// const reference return — caller can read but not modify
[[nodiscard]] const std::string& get_name(size_t index) const {
if (index >= students_.size())
throw std::out_of_range("Invalid student index");
return students_[index];
}
// Non-const method — modifies the object
void add_student(std::string name) {
students_.push_back(std::move(name));
}
// const-correct algorithm usage
[[nodiscard]] bool has_student(std::string_view name) const {
return std::any_of(students_.cbegin(), students_.cend(),
[name](const std::string& s) { return s == name; });
}
private:
std::vector<std::string> students_;
};
void print_registry(const StudentRegistry& reg) {
// Can only call const methods on a const reference
for (size_t i = 0; i < reg.count(); ++i) {
std::cout << reg.get_name(i) << "\n";
}
// reg.add_student("Eve"); // ERROR: cannot call non-const method
}
int main() {
StudentRegistry reg;
reg.add_student("Alice");
reg.add_student("Bob");
print_registry(reg);
// const local variables
const auto count = reg.count(); // immutable
const auto& name = reg.get_name(0); // immutable reference
std::cout << "Count: " << count << ", First: " << name << "\n";
return 0;
}Principes de conception d'API
La règle d'or de la conception d'API en C++, attribuée à Scott Meyers : Rendez les interfaces faciles à utiliser correctement et difficiles à utiliser incorrectement.
Utilisez des types forts : au lieu de void set_timeout(int ms), utilisez void set_timeout(std::chrono::milliseconds timeout). L'appelant ne peut pas passer par erreur des secondes là où vous attendez des millisecondes.
Utilisez [[nodiscard]] : toute fonction qui retourne un code d'erreur, une ressource ou une valeur calculée devrait être [[nodiscard]]. Ignorer la valeur de retour est presque toujours un bug.
Privilégiez la sémantique de valeur : passez les petits types par valeur, les types plus volumineux par const&. Retournez par valeur (le compilateur applique l'élision de copie). Évitez les paramètres de sortie.
Réduisez au minimum la surface publique : chaque membre public est un engagement que vous devez maintenir. Utilisez private par défaut et n'exposez que le nécessaire.
Respectez le principe de moindre surprise : les fonctions doivent faire ce que leur nom indique, rien de plus. Les effets de bord doivent être évidents d'après le nom (save_to_file, pas process).
Conventions de nommage et documentation
Un nommage cohérent rend le code lisible par toute l'équipe. Conventions courantes en C++ :
Types : PascalCase — class HttpClient, struct Point3D, enum class Color
Fonctions/méthodes : snake_case ou camelCase — calculate_total(), getSize()
Variables : snake_case — int item_count, double total_price
Variables membres : underscore final — int count_, std::string name_
Constantes : préfixe k ou UPPER_SNAKE — constexpr int kMaxRetries = 3
Espaces de noms : lowercase — namespace network, namespace detail
Choisissez une convention et appliquez-la à tout le projet (clang-format et clang-tidy peuvent l'automatiser).
Documentation : utilisez des commentaires de style Doxygen pour les API publiques :
/// @brief Se connecte à la base de données.
/// @param connection_string L'URI de connexion à la base de données.
/// @return true si la connexion a réussi.
/// @throws DatabaseError si la connexion échoue.
[[nodiscard]] bool connect(std::string_view connection_string);Profilage des performances
N'optimisez jamais sans avoir d'abord profilé. Ces outils vous indiquent où le temps est réellement passé, et remplacent les suppositions par des données :
# Linux: perf — hardware performance counters
perf record -g ./my_program # record call graph
perf report # interactive flamegraph viewer
# Linux: perf stat — high-level statistics
perf stat ./my_program
# Output: cycles, instructions, cache misses, branch misses, IPC
# macOS: Instruments (via Xcode)
xcrun xctrace record --template 'Time Profiler' --launch ./my_program
# Cross-platform: Google Benchmark (micro-benchmarking)
# In your code:
#include <benchmark/benchmark.h>
static void BM_VectorPushBack(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> v;
for (int i = 0; i < state.range(0); ++i) {
v.push_back(i);
}
benchmark::DoNotOptimize(v.data());
}
}
BENCHMARK(BM_VectorPushBack)->Range(8, 1 << 16);
static void BM_VectorReserved(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> v;
v.reserve(state.range(0));
for (int i = 0; i < state.range(0); ++i) {
v.push_back(i);
}
benchmark::DoNotOptimize(v.data());
}
}
BENCHMARK(BM_VectorReserved)->Range(8, 1 << 16);
BENCHMARK_MAIN();Lors d'une revue de code C++, vérifiez ces problèmes courants :
1. Gestion des ressources : chaque ressource (mémoire, fichiers, verrous) est-elle gérée par RAII ? Reste-t-il des new/delete bruts ?
2. Const-correction : les paramètres, fonctions membres et variables locales sont-ils const lorsque c'est possible ?
3. Gestion des erreurs : les valeurs de retour sont-elles vérifiées ? Les exceptions sont-elles capturées aux niveaux appropriés ? std::expected ou les codes d'erreur sont-ils utilisés de manière cohérente ?
4. Sûreté des threads : les ressources partagées sont-elles protégées ? Des data races sont-elles possibles ? Un sanitizer détecterait-il des problèmes ?
5. Sémantique de déplacement : les objets sont-ils déplacés plutôt que copiés lorsque c'est approprié ? Y a-t-il des appels à std::move sur des objets const (qui copient silencieusement) ?
6. Clarté de l'API : les noms de fonctions sont-ils descriptifs ? Les paramètres sont-ils fortement typés ? [[nodiscard]] est-il utilisé sur les valeurs de retour importantes ?
7. Avertissements de compilation : le code compile-t-il proprement avec -Wall -Wextra -Wpedantic ? Tous les avertissements sont-ils corrigés plutôt que masqués ?
8. Tests : les cas limites sont-ils testés ? Les nouveaux chemins de code sont-ils couverts par des tests ? Les sanitizers passent-ils ?
Déboguer du C++ en production est plus difficile qu'en développement. Préparez-vous-y :
Livrez toujours avec les symboles de débogage : compilez vos binaires de production en RelWithDebInfo. Des symboles de débogage placés dans un fichier .debug séparé n'ajoutent aucun surcoût à l'exécution, mais rendent possible l'analyse des plantages.
Journalisez des données structurées : utilisez une journalisation structurée (JSON) avec des niveaux de log. Incluez les horodatages, les identifiants de thread et les identifiants de requête. Utilisez spdlog ou std::format pour une journalisation rapide et sûre au niveau des types.
Core dumps : activez les core dumps (ulimit -c unlimited) et sachez les analyser avec gdb. Les serveurs de symboles conservent les informations de débogage des binaires déployés.
Bugs courants en production :
- Fuites mémoire — Utilisez ASan dans les environnements de préproduction, ou des outils comme Valgrind / Heaptrack
- Interblocages (deadlocks) — Utilisez TSan pendant les tests, journalisez l'ordre d'acquisition des verrous, définissez des délais d'expiration sur les verrous
- Régressions de performance — Des benchmarks continus en CI détectent les régressions avant le déploiement
- Compatibilité ABI — Modifier la disposition des structs ou les tables de fonctions virtuelles casse l'ABI ; utilisez l'idiome Pimpl pour des ABI stables
- Suivez les C++ Core Guidelines — RAII pour les ressources, règle des 5, typage fort, API publiques minimales
- Rendez tout
constpar défaut — cela documente l'intention, évite des bugs et permet des optimisations - Concevez des API faciles à utiliser correctement et difficiles à utiliser incorrectement — types forts,
[[nodiscard]], sémantique de valeur - Profilez avant d'optimiser — utilisez
perf, Instruments ou Google Benchmark pour trouver les vrais goulots d'étranglement - En revue de code, vérifiez la gestion des ressources, la const-correction, la sûreté des threads et la bonne gestion des erreurs
- Préparez le débogage en production : livrez avec les symboles de débogage, utilisez une journalisation structurée, activez les core dumps
Quiz — Testez vos connaissances
(15 XP)1. Que dit la « règle des 5 » des C++ Core Guidelines ?
2. Quelle est la règle d'or de la conception d'API en C++ ?
3. Pourquoi faut-il profiler avant d'optimiser ?