Bonnes pratiques¶
C++ est un langage puissant mais exigeant. Les meilleures pratiques modernes, codifiees dans les C++ Core Guidelines de Bjarne Stroustrup et Herb Sutter, permettent d'écrire du code sur, lisible et performant. Cette section couvre les conventions, les idiomes modernes, les anti-patterns et la gestion des erreurs.
Conventions et style¶
Deux références de style dominent l'écosystème :
| Guide | Auteur | Focus | URL |
|---|---|---|---|
| C++ Core Guidelines | Stroustrup/Sutter | Idiomes modernes, surete mémoire | isocpp.github.io/CppCoreGuidelines |
| Google C++ Style | Cohérence de code en grande équipe | google.github.io/styleguide | |
| LLVM Coding Std. | LLVM Community | Compilateurs, outils | llvm.org/docs/CodingStandards |
Conventions de nommage courantes (style C++ Core Guidelines) :
// Nommage — conventions recommandees par les Core Guidelines
namespace mon_projet { // snake_case pour les namespaces
class GestionnaireItems { // PascalCase pour les classes
public:
// Methodes en camelCase (Core Guidelines) ou snake_case (Google Style)
void ajouterItem(const std::string& nom, double prix);
// Accesseurs : sans prefixe get
int nombreItems() const { return items_.size(); }
bool estVide() const { return items_.empty(); }
private:
// Membres avec suffixe _ (Core Guidelines) ou prefixe m_ (autre convention)
std::vector<Item> items_;
int compteur_ = 0;
};
// Constantes en SCREAMING_SNAKE_CASE ou kPascalCase (Google)
constexpr int MAX_ITEMS = 1000;
constexpr int kTailleMaxNom = 200;
// Fonctions libres en snake_case
double calculerTaxe(double prix, double taux);
} // namespace mon_projet
RAII — Resource Acquisition Is Initialization¶
RAII est le principe fondateur de la gestion des ressources en C++. Une ressource est acquise dans le constructeur et libérée dans le destructeur. Cela garantit que la ressource est toujours libérée, même en cas d'exception.
// Exemple RAII — wrapper autour d'un descripteur de fichier POSIX
#include <stdexcept>
#include <string>
#include <cstdio>
class FichierRAII {
public:
// Acquisition dans le constructeur
explicit FichierRAII(const std::string& chemin, const char* mode) {
fichier_ = std::fopen(chemin.c_str(), mode);
if (!fichier_)
throw std::runtime_error("Impossible d'ouvrir : " + chemin);
}
// Liberation garantie dans le destructeur
~FichierRAII() {
if (fichier_) std::fclose(fichier_);
}
// Interdit la copie — un seul proprietaire
FichierRAII(const FichierRAII&) = delete;
FichierRAII& operator=(const FichierRAII&) = delete;
// Autorise le deplacement
FichierRAII(FichierRAII&& autre) noexcept : fichier_(autre.fichier_) {
autre.fichier_ = nullptr;
}
FILE* get() const { return fichier_; }
private:
FILE* fichier_ = nullptr;
};
void traiterFichier(const std::string& chemin) {
FichierRAII f(chemin, "r"); // Ouverture
// ... utilisation de f.get() ...
// Fermeture automatique a la sortie du scope, meme si exception
}
Smart pointers — gestion de la propriété¶
Les pointeurs bruts (new/delete) ne doivent apparaître que dans les constructeurs et destructeurs de wrappers RAII. Pour tout le reste, utiliser les smart pointers.
#include <memory>
#include <vector>
#include <string>
struct Noeud {
std::string valeur;
std::unique_ptr<Noeud> gauche; // Propriete exclusive
std::unique_ptr<Noeud> droite;
};
class Cache {
public:
// unique_ptr : un seul proprietaire — transfert avec std::move
void ajouter(std::unique_ptr<Item> item) {
items_.push_back(std::move(item));
}
// shared_ptr : propriete partagee par comptage de references
std::shared_ptr<Item> trouverPartage(int id) const {
for (const auto& item : items_partages_)
if (item->id == id) return item;
return nullptr;
}
// weak_ptr : reference sans propriete (evite les cycles)
void enregistrerObservateur(std::weak_ptr<IObservateur> obs) {
observateurs_.push_back(obs);
}
private:
std::vector<std::unique_ptr<Item>> items_;
std::vector<std::shared_ptr<Item>> items_partages_;
std::vector<std::weak_ptr<IObservateur>> observateurs_;
};
// Creation : toujours via make_unique / make_shared
auto item = std::make_unique<Item>("Widget", 9.99);
auto cache = std::make_shared<Cache>();
// Jamais : new Item("Widget", 9.99)
Move semantics — éviter les copies inutiles¶
Les move semantics (C++11) permettent de "déplacer" les ressources d'un objet plutôt que de les copier, eliminating les allocations superflues.
#include <vector>
#include <string>
#include <utility> // std::move
class Buffer {
public:
explicit Buffer(size_t taille) : data_(taille) {}
// Constructeur de deplacement — vole les donnees de `autre`
Buffer(Buffer&& autre) noexcept : data_(std::move(autre.data_)) {}
// Operateur de deplacement
Buffer& operator=(Buffer&& autre) noexcept {
if (this != &autre) data_ = std::move(autre.data_);
return *this;
}
size_t taille() const { return data_.size(); }
private:
std::vector<char> data_;
};
// Retour par valeur — le compilateur applique NRVO (Named Return Value Optimization)
// ou move semantics : aucune copie du vecteur
std::vector<std::string> chargerNoms() {
std::vector<std::string> noms;
noms.reserve(1000);
for (int i = 0; i < 1000; ++i)
noms.push_back("nom_" + std::to_string(i));
return noms; // NRVO — pas de copie
}
void traiter(std::vector<std::string> noms) { /* ... */ }
int main() {
auto noms = chargerNoms(); // Pas de copie — NRVO
traiter(std::move(noms)); // Pas de copie — move dans traiter()
// noms est en etat valide mais indetermine apres std::move
}
std::optional, std::variant et structured bindings (C++17)¶
#include <optional>
#include <variant>
#include <string>
#include <iostream>
// std::optional — valeur qui peut etre absente (remplace les pointeurs nullables)
std::optional<int> chercher(const std::vector<int>& v, int cible) {
for (int i = 0; i < (int)v.size(); ++i)
if (v[i] == cible) return i;
return std::nullopt;
}
if (auto idx = chercher({1, 5, 3, 7}, 3); idx.has_value())
std::cout << "Trouve a l'index " << *idx << "\n";
// std::variant — union type-safe (remplace les unions C)
using Resultat = std::variant<int, std::string, double>;
Resultat calculer(bool erreur) {
if (erreur) return std::string("Erreur de calcul");
return 42;
}
auto r = calculer(false);
std::visit([](auto&& val) {
std::cout << val << "\n";
}, r);
// Structured bindings (C++17) — decomposition de paires et structs
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [nom, score] : scores) // Decomposition directe
std::cout << nom << ": " << score << "\n";
std::expected — gestion d'erreurs sans exceptions (C++23)¶
// std::expected : retourne soit une valeur, soit une erreur (C++23)
// Evite les exceptions pour les erreurs attendues
#include <expected>
#include <string>
#include <charconv>
enum class ErreurParse { NombreInvalide, DepassementCapacite };
std::expected<int, ErreurParse> parseEntier(std::string_view sv) {
int resultat = 0;
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), resultat);
if (ec == std::errc::invalid_argument)
return std::unexpected(ErreurParse::NombreInvalide);
if (ec == std::errc::result_out_of_range)
return std::unexpected(ErreurParse::DepassementCapacite);
return resultat;
}
void utiliser() {
auto r = parseEntier("42");
if (r)
std::cout << "Valeur : " << *r << "\n";
else if (r.error() == ErreurParse::NombreInvalide)
std::cerr << "Nombre invalide\n";
// Chaining avec and_then / or_else (C++23)
auto resultat = parseEntier("100")
.and_then([](int n) -> std::expected<int, ErreurParse> {
return n * 2;
});
}
Anti-patterns a éviter¶
Raw new/delete — utiliser les smart pointers¶
// MAUVAIS — fuite memoire si exception entre new et delete
void mauvaise_pratique() {
Item* item = new Item("test", 1.0);
traiter(item); // Si cette fonction leve une exception...
delete item; // ...cette ligne n'est jamais atteinte
}
// BON — le destructeur de unique_ptr libere toujours
void bonne_pratique() {
auto item = std::make_unique<Item>("test", 1.0);
traiter(*item); // Exception ou pas, item est libere
}
Casts C — utiliser les casts C++¶
// MAUVAIS — cast C : ignore les avertissements du compilateur
double prix = 9.99;
int prix_int = (int)prix; // Silencieux, potentiellement dangereux
// BON — cast C++ : intentions claires et verifications
int prix_int2 = static_cast<int>(prix); // Conversion numerique explicite
const char* ptr = reinterpret_cast<const char*>(&prix); // Reinterpretation memoire
using namespace std — polluer le namespace global¶
// MAUVAIS — risque de collision de noms, illisible en equipe
using namespace std;
string nom = "test"; // D'ou vient string ? Impossible a determiner
// BON — import selectif ou qualification explicite
using std::string;
using std::cout;
std::vector<int> v = {1, 2, 3}; // Toujours clair
Macros de constantes — utiliser constexpr¶
// MAUVAIS — les macros n'ont pas de type, pas de scope, pas de debug
#define MAX_CONNEXIONS 100
#define CARRE(x) ((x) * (x)) // Risques d'evaluation multiple de x
// BON — constexpr : type-safe, scope, visible au debugger
constexpr int MAX_CONNEXIONS = 100;
template<typename T>
constexpr T carre(T x) { return x * x; } // Inlinee a la compilation
Performance — patterns de base¶
// Cache locality — prefer les structures plates aux pointeurs entrelaces
struct ParticuleAOS { // Array of Structures (mauvais pour SIMD)
float x, y, z;
float vx, vy, vz;
float masse;
};
struct ParticuleSOA { // Structure of Arrays (meilleur cache locality)
std::vector<float> x, y, z;
std::vector<float> vx, vy, vz;
std::vector<float> masse;
};
// reserve() — eviter les reallocation
std::vector<int> v;
v.reserve(10000); // Une seule allocation au lieu de ~14 reallocs
for (int i = 0; i < 10000; ++i)
v.push_back(i);
// emplace_back vs push_back — construction directe sans copie intermediaire
std::vector<std::string> noms;
noms.push_back("Alice"); // Construit une string, puis copie/move
noms.emplace_back("Bob"); // Construit directement dans le vecteur
// constexpr — calculs a la compilation
constexpr double PI = 3.14159265358979323846;
constexpr double AIRE_CERCLE_R5 = PI * 5.0 * 5.0; // Evalue a la compilation
Rule of Five
En C++ moderne, si vous definissez l'un parmi le destructeur, le constructeur de copie, l'opérateur de copie, le constructeur de déplacement ou l'opérateur de déplacement — definissez les cinq (ou signalez leur absence avec = delete ou = default). Un oubli conduit souvent a des doubles liberations ou des fuites mémoire.
std::string_view en parametre de fonction
Privilegiez std::string_view sur const std::string& pour les parametres de lecture seule. string_view accepte les std::string, les litteraux, et les sous-chaînes sans copie. Exception : si la fonction doit stocker la valeur au-delà de la durée de vie de l'appelant, utiliser std::string par valeur.