Gestion des Ressources · C++

RAII

Resource Acquisition Is Initialization

Liez l'acquisition d'une ressource à la durée de vie d'un objet. Le RAII est l'idiome fondamental du C++ moderne pour écrire du code sûr, sans fuites et exception-safe.

explorer

Le cycle de vie RAII

Chaque ressource suit un cycle déterministe, garanti par le compilateur.

01

Acquisition

La ressource est acquise dans le constructeur. Si l'acquisition échoue, une exception est levée.

02

Utilisation

L'objet encapsule la ressource et expose une API sûre. Pas d'accès direct à la ressource brute.

03

Libération

Le destructeur libère la ressource automatiquement, même en cas d'exception.

04

Exception Safety

Le stack unwinding appelle les destructeurs. Zéro fuite, même sur un chemin d'erreur.

⚡ Garantie du compilateur

En C++, le destructeur est appelé déterminiquement à la fin du scope. Contrairement à un garbage collector, il n'y a aucune latence, aucune incertitude — la ressource est libérée immédiatement.

Le Principe RAII

Acquérir dans le constructeur, libérer dans le destructeur

Sans RAII, le développeur doit manuellement allouer et libérer chaque ressource. Un simple return prématuré ou une exception suffit à créer une fuite mémoire. Avec RAII, le compilateur garantit la libération.

manual.cpp Sans RAII
void process()
{
    // Allocation manuelle
    int* data = new int[1024];

    if (!validate(data)) {
        return;  // FUITE ! delete oublié
    }

    transform(data);
    // Si transform() lève une exception
    // → FUITE aussi !

    delete[] data;  // Atteint seulement
                      // si tout va bien
}
raii.cpp Avec RAII
void process()
{
    // Le vector gère sa mémoire
    std::vector<int> data(1024);

    if (!validate(data)) {
        return;  // OK : destructeur appelé
    }

    transform(data);
    // Si transform() lève une exception
    // → destructeur appelé quand même

    // Pas de delete — le destructeur
    // de vector libère tout
}
💡

Règle d'or : Ne jamais utiliser new/delete directement. Préférez std::vector, std::string, std::unique_ptr — des conteneurs RAII qui libèrent automatiquement leurs ressources.

Smart Pointers

Propriété automatique de la mémoire dynamique

Les smart pointers encapsulent un pointeur brut et libèrent la mémoire dans leur destructeur. std::unique_ptr pour la propriété exclusive, std::shared_ptr pour la propriété partagée.

unique_ptr.cpp unique_ptr
#include <memory>

class Texture {
public:
    Texture(const std::string& path);
    void bind() const;
};

void render()
{
    // Propriété exclusive, zéro coût
    auto tex = std::make_unique<Texture>(
        "wall.png"
    );

    tex->bind();

    // Pas de delete.
    // unique_ptr libère à la fin du scope.
}

// Transfert de propriété (move semantics)
std::unique_ptr<Texture> load(const std::string& p)
{
    return std::make_unique<Texture>(p);
}
shared_ptr.cpp shared_ptr
#include <memory>

class Config {
public:
    explicit Config(const std::string& file);
    std::string get(const std::string& key) const;
};

// Propriété partagée : plusieurs modules
// référencent le même Config
auto cfg = std::make_shared<Config>(
    "app.toml"
);

module_a(cfg);  // copie du shared_ptr
module_b(cfg);  // ref count = 3

// Quand le dernier shared_ptr est détruit
// → Config est libéré automatiquement

// weak_ptr pour casser les cycles :
std::weak_ptr<Config> observer = cfg;
if (auto locked = observer.lock()) {
    locked->get("key");
}

Préférez unique_ptr par défaut. Il a zéro overhead par rapport à un pointeur brut. N'utilisez shared_ptr que si la propriété est réellement partagée. Le comptage de références a un coût atomique.

🔒

Lock Guards

Verrouillage automatique des mutex

Un std::lock_guard verrouille le mutex à la construction et le déverrouille à la destruction. Impossible d'oublier le unlock(), même en cas d'exception.

manual_lock.cpp Sans RAII
std::mutex mtx;
std::vector<int> shared_data;

void add(int value)
{
    mtx.lock();

    if (value < 0) {
        return;  // DEADLOCK !
                 // unlock() jamais appelé
    }

    shared_data.push_back(value);
    // Si push_back() lève (bad_alloc)
    // → DEADLOCK aussi !

    mtx.unlock();
}
lock_guard.cpp Avec RAII
std::mutex mtx;
std::vector<int> shared_data;

void add(int value)
{
    // Verrouillage automatique
    std::lock_guard<std::mutex> lock(mtx);

    if (value < 0) {
        return;  // OK : unlock automatique
    }

    shared_data.push_back(value);
    // Si exception → unlock automatique

    // Fin du scope → destructeur
    // → unlock() garanti
}

// C++17 : déduction de template
std::lock_guard lock(mtx);

// Pour plus de flexibilité :
std::unique_lock ulock(mtx);
ulock.unlock();  // libération anticipée
🔎

Choisir le bon guard : std::lock_guard pour le cas simple (lock/unlock automatique), std::unique_lock si vous avez besoin de déverrouiller avant la fin du scope ou d'utiliser std::condition_variable. std::scoped_lock (C++17) pour verrouiller plusieurs mutex sans deadlock.

📄

File Handles

Gestion automatique des fichiers et descripteurs

std::fstream est déjà RAII : le fichier est fermé dans le destructeur. Pour les API C (FILE*, sockets, handles OS), un wrapper RAII garantit la fermeture même en cas d'erreur.

file_c_style.cpp Style C
#include <cstdio>

void export_data()
{
    FILE* f = fopen("out.csv", "w");
    if (!f) return;

    fprintf(f, "id,name\n");

    auto rows = fetch_rows();
    // Si fetch_rows() lève une exception
    // → fclose() jamais appelé
    // → descripteur fuité

    for (auto& r : rows) {
        fprintf(f, "%d,%s\n", r.id, r.name);
    }

    fclose(f);  // facile à oublier
}
file_raii.cpp RAII
#include <fstream>

void export_data()
{
    // fstream = RAII natif
    std::ofstream out("out.csv");
    if (!out) return;

    out << "id,name\n";

    for (auto& r : fetch_rows()) {
        out << r.id << "," << r.name << "\n";
    }

    // Le destructeur de ofstream
    // ferme le fichier automatiquement.
    // Même en cas d'exception :
    // stack unwinding → ~ofstream()
}

// Pour un FILE* legacy, unique_ptr :
auto fp = std::unique_ptr<FILE, decltype(&fclose)>(
    fopen("data.bin", "rb"), &fclose
);
📚

Astuce : std::unique_ptr accepte un custom deleter. Vous pouvez ainsi wrapper n'importe quelle ressource C (FILE*, sqlite3*, SDL_Window*) sans écrire une classe complète.

🛠

Custom RAII Wrappers

Écrivez vos propres gardiens de ressources

Quand la bibliothèque standard ne suffit pas, créez votre propre wrapper RAII. Le pattern est toujours le même : acquérir dans le constructeur, libérer dans le destructeur, interdire la copie ou implémenter le move.

gl_buffer.hpp Custom RAII
#include <GL/gl.h>
#include <utility>

class GlBuffer
{
public:
    // Constructeur : acquisition de la ressource GPU
    GlBuffer()
    {
        glGenBuffers(1, &id_);
    }

    // Destructeur : libération de la ressource GPU
    ~GlBuffer()
    {
        if (id_ != 0) {
            glDeleteBuffers(1, &id_);
        }
    }

    // Interdire la copie (une ressource GPU = un propriétaire)
    GlBuffer(const GlBuffer&) = delete;
    GlBuffer& operator=(const GlBuffer&) = delete;

    // Autoriser le move (transfert de propriété)
    GlBuffer(GlBuffer&& other) noexcept
        : id_(std::exchange(other.id_, 0))
    {}

    GlBuffer& operator=(GlBuffer&& other) noexcept
    {
        if (this != &other) {
            if (id_ != 0) glDeleteBuffers(1, &id_);
            id_ = std::exchange(other.id_, 0);
        }
        return *this;
    }

    GLuint id() const noexcept { return id_; }

private:
    GLuint id_ = 0;
};
🎯

Pattern clé : std::exchange(other.id_, 0) dans le move constructor met l'objet source dans un état « vide » valide. Son destructeur ne libérera rien — la propriété a été transférée, pas dupliquée.

Rule of Five & Rule of Zero

Quand et comment définir les opérations spéciales

La Rule of Five dit : si vous définissez l'un des 5 membres spéciaux (destructeur, copy/move constructor, copy/move assignment), définissez-les tous. La Rule of Zero dit : préférez ne rien définir — utilisez des types RAII et le compilateur fait le reste.

rule_of_five.cpp Rule of Five
// Quand vous gérez une ressource brute :
// définissez les 5 opérations spéciales

class Buffer
{
public:
    explicit Buffer(size_t size)
        : data_(new char[size])
        , size_(size) {}

    // 1. Destructeur
    ~Buffer() { delete[] data_; }

    // 2. Copy constructor
    Buffer(const Buffer& o)
        : data_(new char[o.size_])
        , size_(o.size_)
    {
        std::copy(o.data_, o.data_ + size_, data_);
    }

    // 3. Copy assignment
    Buffer& operator=(const Buffer& o) {
        if (this != &o) {
            delete[] data_;
            size_ = o.size_;
            data_ = new char[size_];
            std::copy(o.data_, o.data_ + size_, data_);
        }
        return *this;
    }

    // 4. Move constructor
    Buffer(Buffer&& o) noexcept
        : data_(std::exchange(o.data_, nullptr))
        , size_(std::exchange(o.size_, 0)) {}

    // 5. Move assignment
    Buffer& operator=(Buffer&& o) noexcept {
        if (this != &o) {
            delete[] data_;
            data_ = std::exchange(o.data_, nullptr);
            size_ = std::exchange(o.size_, 0);
        }
        return *this;
    }

private:
    char* data_;
    size_t size_;
};
rule_of_zero.cpp Rule of Zero
#include <vector>
#include <string>
#include <memory>

// AUCUNE opération spéciale définie.
// Le compilateur génère tout correctement
// car chaque membre est déjà RAII.

class Document
{
public:
    Document(std::string title,
             std::string content)
        : title_(std::move(title))
        , content_(std::move(content))
    {}

    void add_tag(std::string tag)
    {
        tags_.push_back(std::move(tag));
    }

private:
    std::string              title_;
    std::string              content_;
    std::vector<std::string> tags_;
};

// Document est copiable, movable,
// et ne fuit jamais — sans écrire
// une seule ligne de gestion mémoire.

// Préférez toujours la Rule of Zero.
// N'écrivez la Rule of Five que pour
// les classes « ressource » de bas niveau.
💡

En pratique : 95% de vos classes devraient suivre la Rule of Zero. Composez vos classes avec std::string, std::vector, std::unique_ptr, et le compilateur génère les opérations spéciales correctes. La Rule of Five ne concerne que les wrappers de ressources brutes.