Le cycle de vie RAII
Chaque ressource suit un cycle déterministe, garanti par le compilateur.
Acquisition
La ressource est acquise dans le constructeur. Si l'acquisition échoue, une exception est levée.
Utilisation
L'objet encapsule la ressource et expose une API sûre. Pas d'accès direct à la ressource brute.
Libération
Le destructeur libère la ressource automatiquement, même en cas d'exception.
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
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.
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
}
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
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.
#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);
}
#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
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.
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();
}
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
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.
#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
}
#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
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.
#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
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.
// 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_;
};
#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.