Aller au contenu

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 Google 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.