🔝 Retour au Sommaire
En section 9.2, nous avons vu qu'un cycle de shared_ptr crée un memory leak structurel : deux objets (ou plus) se référencent mutuellement, empêchant leurs compteurs de références d'atteindre zéro. Dans cette section, nous allons disséquer le problème en profondeur, identifier les structures de données qui y sont vulnérables, et maîtriser std::weak_ptr comme outil de résolution.
Reprenons l'exemple classique et traçons l'état des compteurs à chaque étape :
#include <memory>
struct Personne {
std::string nom;
std::shared_ptr<Personne> ami; // ⚠️ Référence forte bidirectionnelle
Personne(std::string n) : nom(std::move(n)) {
std::print("[+] {} créé\n", nom);
}
~Personne() {
std::print("[-] {} détruit\n", nom);
}
};
void creer_cycle() {
auto alice = std::make_shared<Personne>("Alice");
// Alice strong_count = 1 (variable alice)
auto bob = std::make_shared<Personne>("Bob");
// Bob strong_count = 1 (variable bob)
alice->ami = bob;
// Bob strong_count = 2 (variable bob + alice->ami)
bob->ami = alice;
// Alice strong_count = 2 (variable alice + bob->ami)
} // Fin du scope :
// alice détruit → Alice strong_count passe de 2 à 1 (bob->ami tient encore)
// bob détruit → Bob strong_count passe de 2 à 1 (alice->ami tient encore)
//
// Alice strong_count = 1 → PAS libéré
// Bob strong_count = 1 → PAS libéré
//
// Output :
// [+] Alice créé
// [+] Bob créé
// (silence — aucun destructeur n'est appelé)Le cycle forme un verrouillage mutuel : Alice maintient Bob en vie, Bob maintient Alice en vie, et personne ne peut mourir en premier. La mémoire est irrémédiablement perdue pour le reste de l'exécution du programme.
Après la fin du scope — état en mémoire :
┌──────────────────────────────────────┐
│ │
▼ │
┌────────────┐ ami (shared_ptr) ┌────────────┐
│ Alice │ ──────────────────────> │ Bob │
│ strong: 1 │ │ strong: 1 │
│ │ <────────────────────── │ │
└────────────┘ ami (shared_ptr) └────────────┘
▲ │
│ │
└──────────────────────────────────────┘
Aucune variable externe ne pointe vers ces objets.
Ils sont inaccessibles mais non libérés → MEMORY LEAK
Les cycles ne se limitent pas aux relations bidirectionnelles entre deux objets. Ils apparaissent dans toute structure où des shared_ptr forment une boucle, quelle que soit sa longueur.
Le cas le plus évident — deux objets se référencent mutuellement, comme l'exemple Alice/Bob ci-dessus. On le rencontre typiquement dans les relations parent↔enfant, les listes doublement chaînées, ou les associations bidirectionnelles.
Trois objets ou plus forment une chaîne circulaire :
struct Noeud {
std::string id;
std::shared_ptr<Noeud> suivant;
explicit Noeud(std::string i) : id(std::move(i)) {}
~Noeud() { std::print("[-] {} détruit\n", id); }
};
void cycle_triangulaire() {
auto a = std::make_shared<Noeud>("A");
auto b = std::make_shared<Noeud>("B");
auto c = std::make_shared<Noeud>("C");
a->suivant = b; // A → B
b->suivant = c; // B → C
c->suivant = a; // C → A → cycle de longueur 3
} // Aucun destructeur n'est appelé — les trois objets fuientUn objet qui se référence lui-même :
void auto_reference() {
auto noeud = std::make_shared<Noeud>("X");
noeud->suivant = noeud; // X → X → auto-cycle
} // noeud détruit → strong_count passe de 2 à 1
// L'objet se maintient en vie tout seul → LEAK| Structure | Source du cycle | Fréquence |
|---|---|---|
| Arbre avec pointeur parent | enfant → parent + parent → enfant | Très fréquent |
| Liste doublement chaînée | nœud → suivant + nœud → précédent | Fréquent |
| Graphe (orienté ou non) | arêtes entre nœuds | Fréquent |
| Observer pattern | observable → observateur + observateur → observable | Courant |
| Cache avec back-reference | objet cache → propriétaire → cache | Subtil |
| Callbacks / closures | callback capture un shared_ptr vers l'objet qui détient le callback |
Très subtil |
std::weak_ptr brise les cycles en remplaçant l'une des références fortes par une référence faible — une référence qui observe la ressource sans la posséder. Le weak_ptr ne contribue pas au strong count : il ne prolonge pas la durée de vie de la ressource.
struct Personne {
std::string nom;
std::weak_ptr<Personne> ami; // ✅ Référence faible — pas de cycle
Personne(std::string n) : nom(std::move(n)) {
std::print("[+] {} créé\n", nom);
}
~Personne() {
std::print("[-] {} détruit\n", nom);
}
};
void sans_cycle() {
auto alice = std::make_shared<Personne>("Alice");
auto bob = std::make_shared<Personne>("Bob");
alice->ami = bob; // weak_ptr → n'incrémente PAS le strong_count de Bob
bob->ami = alice; // weak_ptr → n'incrémente PAS le strong_count d'Alice
// Alice strong_count = 1 (variable alice uniquement)
// Bob strong_count = 1 (variable bob uniquement)
} // bob détruit → Bob strong_count passe de 1 à 0 → Bob libéré ✅
// alice détruit → Alice strong_count passe de 1 à 0 → Alice libérée ✅
//
// Output :
// [+] Alice créé
// [+] Bob créé
// [-] Bob détruit
// [-] Alice détruitLe cycle est brisé parce que les liens ami ne maintiennent plus les objets en vie. Quand les variables locales sortent du scope, chaque compteur tombe à zéro normalement.
Dans une relation bidirectionnelle, la question est : quel lien doit être fort, et lequel doit être faible ? La réponse repose sur la hiérarchie de propriété :
- Le propriétaire utilise un
shared_ptrvers la ressource possédée (lien fort, direction « vers le bas »). - La ressource possédée utilise un
weak_ptrvers son propriétaire (lien faible, direction « vers le haut »).
shared_ptr (propriété)
Parent ─────────────────────────────> Enfant
<─────────────────────────────
weak_ptr (observation)
Dans un arbre, le parent possède ses enfants (forte), les enfants observent leur parent (faible). Dans un observer pattern, l'observable possède la liste des observateurs (forte), chaque observateur observe l'observable (faible).
Quand la relation est symétrique (deux nœuds d'un graphe, deux amis), le choix du sens est arbitraire — l'important est de briser le cycle en rendant au moins un lien faible.
Un weak_ptr ne se crée jamais à partir de new ou make_shared. Il se crée exclusivement à partir d'un shared_ptr (ou d'un autre weak_ptr) :
auto shared = std::make_shared<std::string>("Hello");
// Création depuis un shared_ptr
std::weak_ptr<std::string> w1 = shared;
std::weak_ptr<std::string> w2(shared);
// Copie depuis un autre weak_ptr
std::weak_ptr<std::string> w3 = w1;
// Le strong_count n'a pas changé
std::print("strong_count = {}\n", shared.use_count()); // 1lock() est la méthode centrale de weak_ptr. Elle tente de promouvoir le weak_ptr en shared_ptr :
- Si la ressource existe encore (strong count > 0),
lock()retourne unshared_ptrvalide et incrémente le strong count. - Si la ressource a été détruite (strong count == 0),
lock()retourne unshared_ptrnul.
L'opération est atomique : il n'y a pas de race condition entre le test et la promotion.
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// Cas 1 : ressource encore vivante
if (auto locked = weak.lock()) {
std::print("Valeur : {}\n", *locked); // 42
std::print("strong_count = {}\n", shared.use_count()); // 2 (shared + locked)
}
// locked détruit → strong_count revient à 1
// Cas 2 : ressource détruite
shared.reset(); // strong_count → 0 → int(42) libéré
if (auto locked = weak.lock()) {
std::print("Ceci ne sera jamais affiché\n");
} else {
std::print("Ressource expirée\n"); // ✅ Affiché
}Le pattern if (auto locked = weak.lock()) est idiomatique et devrait être votre réflexe systématique pour accéder à une ressource via weak_ptr.
expired() retourne true si la ressource a été détruite (strong count == 0) :
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
std::print("Expiré ? {}\n", weak.expired()); // false
shared.reset();
std::print("Expiré ? {}\n", weak.expired()); // true
⚠️ Attention en multi-thread.expired()est sujet à une race condition : la ressource peut être détruite par un autre thread immédiatement après queexpired()ait retournéfalse. Ne l'utilisez jamais comme garde avant unlock()— utilisezlock()directement :
// ❌ Race condition entre expired() et lock()
if (!weak.expired()) {
auto locked = weak.lock(); // Peut retourner nullptr ici !
*locked; // 💥 UB si locked est nul
}
// ✅ lock() est atomique — une seule opération
if (auto locked = weak.lock()) {
*locked; // Garanti valide
}std::weak_ptr<int> weak = shared;
weak.reset(); // weak ne référence plus rien
// Le weak_count sur le bloc de contrôle est décrémentéauto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// Retourne le strong_count (pas le weak_count)
std::print("Propriétaires forts : {}\n", weak.use_count()); // 1Le cas d'usage le plus classique. Le parent possède ses enfants, les enfants observent leur parent :
struct TreeNode {
std::string valeur;
std::weak_ptr<TreeNode> parent; // ✅ Faible vers le haut
std::vector<std::shared_ptr<TreeNode>> enfants; // Fort vers le bas
explicit TreeNode(std::string v) : valeur(std::move(v)) {}
void ajouter_enfant(std::shared_ptr<TreeNode> enfant) {
enfant->parent = std::shared_ptr<TreeNode>(
// Comment obtenir un shared_ptr vers this ? Voir encadré ci-dessous.
);
enfants.push_back(std::move(enfant));
}
std::string chemin() const {
std::string resultat = valeur;
// Remonter vers la racine via les weak_ptr
auto p = parent.lock();
while (p) {
resultat = p->valeur + "/" + resultat;
p = p->parent.lock();
}
return resultat;
}
};💡 enable_shared_from_this — Dans l'exemple ci-dessus,
ajouter_enfanta besoin d'unshared_ptrversthispour l'assigner auparentde l'enfant. C'est le rôle destd::enable_shared_from_this, une classe de base qui fournit la méthodeshared_from_this(). Voici la version correcte :
struct TreeNode : public std::enable_shared_from_this<TreeNode> {
std::string valeur;
std::weak_ptr<TreeNode> parent;
std::vector<std::shared_ptr<TreeNode>> enfants;
explicit TreeNode(std::string v) : valeur(std::move(v)) {}
void ajouter_enfant(std::shared_ptr<TreeNode> enfant) {
enfant->parent = shared_from_this(); // ✅ shared_ptr vers this
enfants.push_back(std::move(enfant));
}
};
// Utilisation
auto racine = std::make_shared<TreeNode>("racine");
auto fils = std::make_shared<TreeNode>("fils");
racine->ajouter_enfant(fils);
std::print("{}\n", fils->chemin()); // "racine/fils"
⚠️ shared_from_this()ne fonctionne que si l'objet est déjà géré par unshared_ptr. L'appeler sur un objet stack ou un objet géré parunique_ptrest un comportement indéfini.
Un cache stocke des weak_ptr vers des ressources coûteuses. Si la ressource est encore utilisée ailleurs, le cache peut la retrouver instantanément. Si plus personne ne l'utilise, elle a été automatiquement libérée et le cache le détecte :
class TextureCache {
std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;
public:
std::shared_ptr<Texture> obtenir(const std::string& chemin) {
// Tenter de récupérer depuis le cache
auto it = cache_.find(chemin);
if (it != cache_.end()) {
if (auto texture = it->second.lock()) {
std::print("[cache] Hit pour '{}'\n", chemin);
return texture; // ✅ Encore en vie — réutilisation
}
// Le weak_ptr a expiré — nettoyer l'entrée
cache_.erase(it);
}
// Charger depuis le disque
std::print("[cache] Miss pour '{}' — chargement\n", chemin);
auto texture = std::make_shared<Texture>(chemin);
cache_[chemin] = texture; // Stocker un weak_ptr
return texture;
}
void purger_expires() {
std::erase_if(cache_, [](const auto& pair) {
return pair.second.expired();
});
}
};Avec ce design, le cache n'empêche jamais la libération d'une texture inutilisée. La mémoire est récupérée dès que le dernier utilisateur relâche son shared_ptr. Le cache sert uniquement d'accélérateur pour les ressources encore vivantes.
Un système d'événements où l'observable ne prolonge pas la durée de vie des observateurs :
class EventEmitter {
std::vector<std::weak_ptr<EventListener>> listeners_;
public:
void ajouter(std::shared_ptr<EventListener> listener) {
listeners_.push_back(listener); // Stocke un weak_ptr
}
void emettre(const Event& event) {
// Parcours avec nettoyage des observateurs expirés
auto it = listeners_.begin();
while (it != listeners_.end()) {
if (auto listener = it->lock()) {
listener->on_event(event); // ✅ Encore vivant
++it;
} else {
it = listeners_.erase(it); // Nettoyage automatique
}
}
}
};Si un observateur est détruit (son shared_ptr n'est plus détenu par personne), l'émetteur le détecte au prochain emettre() et le retire silencieusement de la liste. Aucune fuite, aucun dangling pointer.
Un piège subtil survient quand une lambda capture un shared_ptr vers l'objet qui détient cette lambda :
class Worker : public std::enable_shared_from_this<Worker> {
std::function<void()> callback_;
public:
void demarrer() {
// ❌ CYCLE : callback_ capture shared_ptr → Worker → callback_
callback_ = [self = shared_from_this()]() {
self->traiter();
};
}
void demarrer_safe() {
// ✅ PAS DE CYCLE : capture un weak_ptr
callback_ = [weak_self = weak_from_this()]() {
if (auto self = weak_self.lock()) {
self->traiter();
}
};
}
void traiter() { std::print("Traitement\n"); }
};weak_from_this() (disponible depuis C++17) est le pendant faible de shared_from_this(). Il retourne un weak_ptr vers l'objet courant, idéal pour les captures de lambda.
Les cycles de références ne produisent ni erreur de compilation ni crash à l'exécution. Ils fuient silencieusement. Voici les outils pour les détecter.
Valgrind détecte la mémoire non libérée en fin de programme, y compris les fuites causées par des cycles :
$ valgrind --leak-check=full ./mon_programme
==12345== 96 bytes in 2 blocks are definitely lost in loss record 1 of 1
==12345== at 0x...: operator new(unsigned long)
==12345== by 0x...: std::make_shared<Personne>(...)Le message « definitely lost » signale une fuite. Si les objets impliqués sont des shared_ptr, un cycle est la cause probable. Voir section 5.5.1 pour l'utilisation détaillée de Valgrind.
Compilez avec -fsanitize=address pour activer la détection de fuites intégrée :
$ g++ -std=c++20 -fsanitize=address -g main.cpp -o main
$ ./main
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 48 byte(s) in 1 object(s) allocated from:
...Voir section 5.5.2.
La meilleure détection reste la prévention. Lors d'une revue de code, posez-vous ces questions :
- Un objet A stocke-t-il un
shared_ptrvers B, et B (directement ou indirectement) unshared_ptrvers A ? - Une lambda capture-t-elle un
shared_ptrvers l'objet qui détient cette lambda ? - Une structure de données forme-t-elle un graphe avec des cycles potentiels ?
Si la réponse est oui à l'une de ces questions, au moins un lien doit devenir un weak_ptr.
std::weak_ptr<Data> weak = /* ... */;
// ❌ Aucune vérification — crash si expiré
auto shared = weak.lock();
shared->traiter(); // 💥 UB si shared est nul
// ✅ Toujours vérifier
if (auto shared = weak.lock()) {
shared->traiter();
}Le shared_ptr retourné par lock() prolonge la durée de vie de la ressource tant qu'il existe. Si vous le stockez dans un membre de classe, vous recréez potentiellement un lien fort — et donc un cycle :
class Observer {
std::shared_ptr<Subject> subject_; // ❌ Lien fort recréé
void actualiser() {
// Le weak_ptr original est contourné
subject_ = weak_subject_.lock(); // Stocké en membre → lien fort permanent
}
};Le résultat de lock() devrait être une variable locale temporaire, utilisée et détruite dans le même scope :
class Observer {
std::weak_ptr<Subject> subject_; // ✅ Lien faible
void actualiser() {
if (auto s = subject_.lock()) { // Temporaire local
s->lire_etat();
} // s détruit ici → strong_count redescend
}
};C'est impossible — et c'est logique. Un weak_ptr a besoin du bloc de contrôle pour fonctionner, et seul un shared_ptr possède un bloc de contrôle :
int* raw = new int(42);
// std::weak_ptr<int> w(raw); // ❌ Ne compile pas
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> w = shared; // ✅ Via un shared_ptr | Concept | Détail |
|---|---|
| Cause des cycles | Boucle de shared_ptr (longueur 1, 2, ou N) empêchant les compteurs d'atteindre 0 |
| Conséquence | Memory leak silencieux — pas de crash, pas de warning |
| Solution | Remplacer au moins un lien fort par un weak_ptr |
| Règle de direction | Liens forts vers le bas (parent → enfant), liens faibles vers le haut (enfant → parent) |
| lock() | Promotion atomique weak_ptr → shared_ptr. Retourne nullptr si expiré. |
| expired() | Vérification rapide mais non thread-safe. Préférer lock(). |
| enable_shared_from_this | Permet d'obtenir un shared_ptr/weak_ptr vers this |
| Détection | Valgrind, LeakSanitizer, revue de code |
| Patterns clés | Arbres parent↔enfant, caches, observer, callbacks avec capture |