Programmation • IoT

Chapitre 5 – std::shared_ptr et std::weak_ptr

Document réservé

Vous consultez actuellement la présentation publique de ce chapitre.

Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.

Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).

5 std::shared_ptr et std::weak_ptr 5.1 Quand un propriétaire unique ne suffit plus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3 Le comptage de références . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.5 Une expérience avec un objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.6 Copier ou déplacer un shared_ptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.7 Passer un shared_ptr à une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.9 Le problème des références circulaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.10 Le problème vient de la propriété . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.11 std::weak_ptr : observer sans posséder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.12 Pourquoi un weak_ptr ne peut-il pas être déréférencé directement ? . . . . . . . . . . . . . 5.13 Tester l’existence de la ressource . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.15 Rompre une référence circulaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.16 Les trois modèles de relation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.17 Quel pointeur intelligent choisir ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2026 – C++ Partie III std::shared_ptr et std::weak_ptr
Accueil
std::shared_ptr et std::weak_ptr Quand un propriétaire unique ne suffit plus Dans le chapitre précédent, nous avons étudié : qui exprime une propriété exclusive. À un instant donné, une ressource possède un seul propriétaire et cette situation est généralement souhaitable, car elle permet de répondre sans ambiguïté à la question : Qui est responsable de la destruction de la ressource ? Mais certaines architectures nécessitent réellement que plusieurs objets partagent la propriété d’une même ressource. Considérons par exemple un objet représentant des données utilisées simultanément par plusieurs composants d’un programme. Nous voulons alors obtenir une situation de la forme : La ressource doit continuer d’exister tant qu’au moins un de ces objets en a encore besoin et ne doit être détruite que lorsque le dernier propriétaire disparaît. C’est le rôle de : Le principe de shared_ptr Un shared_ptr est un pointeur intelligent permettant à plusieurs objets de partager la propriété d’une même ressource mais contrairement à unique_ptr, un shared_ptr peut être copié. auto p1 = std::make_shared<int>(42); Nous pouvons ensuite écrire : 2026 – C++ Partie III std::shared_ptr et std::weak_ptr Cette fois, la copie est autorisée et après cette opération, p1 et p2 désignent le même entier dynamique et en partagent la propriété : La question devient alors : Quand faut-il détruire l’entier ? La réponse repose sur un mécanisme appelé comptage de références. Le comptage de références Un shared_ptr participe à un mécanisme qui comptabilise le nombre de propriétaires partageant la même auto p1 = std::make_shared<int>(42); il existe un propriétaire par conséquent le compteur vaut donc conceptuellement : il existe deux propriétaires et le compteur vaut Si p2 est détruit, le compteur redescend à : La ressource ne doit pas encore être détruite puisque p1 la possède toujours. Lorsque p1 est finalement détruit, le nombre de propriétaires devient : La ressource est alors automatiquement détruite.
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr Observer le compteur La fonction membre : permet d’observer le nombre de propriétaires. auto p1 = std::make_shared<int>(42); std::cout << p1.use_count() << ’\n’; std::cout << p1.use_count() << ’\n’; std::cout << p2.use_count() << ’\n’; std::cout << p1.use_count() << ’\n’; Le programme affiche : Lorsque p2 arrive à la fin de sa portée, il est détruit et le compteur passe alors automatiquement de 2 à 1. Comme pour unique_ptr, il existe une fonction permettant de construire directement l’objet géré : Nous préférerons donc généralement : auto p = std::make_shared<Point>(3.0, 4.0); à une construction explicite à partir de new. std::shared_ptr<Point> et l’objet Point est automatiquement détruit lorsque le dernier propriétaire disparaît.
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr Une expérience avec un objet Pour rendre la destruction visible, utilisons une classe simple : std::cout << « Construction\n »; std::cout << « Destruction\n »; auto p1 = std::make_shared<Objet>(); std::cout << p1.use_count() << ’\n’; std::cout << p1.use_count() << ’\n’; Nous obtenons conceptuellement : La destruction n’intervient pas lorsque p2 ou p3 disparaissent mais intervient uniquement lorsque le dernier propriétaire, p1, est détruit. • Un shared_ptr utilise un mécanisme de comptage de propriétaires. • La ressource reste vivante tant que le nombre de propriétaires est supérieur à zéro. • Lorsque le dernier propriétaire disparaît, la ressource est automatiquement détruite. Copier ou déplacer un shared_ptr Un shared_ptr peut être copié : auto p1 = std::make_shared<int>(42);
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr et après cette copie on a : Les deux pointeurs sont propriétaires cependant, on peut également considérer un déplacement de shared_ptr auto p3 = std::move(p2); Dans ce cas, la propriété détenue par p2 est transférée à p3 et p2 devient vide. Le déplacement n’ajoute donc pas un nouveau propriétaire mais transfère simplement une participation existante à la propriété partagée. Passer un shared_ptr à une fonction Comme avec unique_ptr, le type du paramètre doit exprimer l’intention de la fonction. Par exemple, si une fonction doit seulement utiliser l’objet sans participer à sa propriété, il est généralement inutile de lui transmettre un shared_ptr par valeur. Nous pouvons simplement écrire void afficher(const Point& point) auto p = std::make_shared<Point>(3.0, 4.0); En revanche, si la fonction doit conserver une propriété partagée, elle peut recevoir : void conserver(std::shared_ptr<Point> p) Dans ce cas, l’instruction copie le shared_ptr et le compteur de propriétaires augmente donc temporairement ou durablement selon ce que la fonction fait de cette copie. Contrairement à unique_ptr, aucun std::move n’est nécessaire pour partager la propriété. shared_ptr a un coût La propriété partagée est plus complexe que la propriété exclusive car un shared_ptr doit notamment gérer des informations permettant de connaître le nombre de propriétaires de la ressource. Ces informations sont conservées dans une structure associée, appelée bloc de contrôle. Ce bloc contient notamment les informations nécessaires au comptage des propriétaires. Nous pouvons représenter la situation ainsi :
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr shared_ptr −→ bloc de contrôle −→ ressource Plusieurs shared_ptr peuvent utiliser le même bloc de contrôle par conséquent, la copie et la destruction d’un shared_ptr nécessitent donc notamment la mise à jour du compteur cq qui fait que cette propriété partagée possède ainsi un coût supérieur à celui d’une propriété exclusive. • shared_ptr ne doit pas être considéré comme une version “plus puissante” de unique_ptr. • Il répond à un problème différent et possède un coût supplémentaire. • Lorsque la propriété exclusive suffit, unique_ptr doit généralement être préféré. Le problème des références circulaires Le comptage de références semble résoudre élégamment le problème de la propriété partagée mais il possède une faiblesse importante si nous considérons deux objets qui possèdent chacun l’autre. Soit la classe simplifiée : std::shared_ptr<Noeud> autre; std::cout << « Destruction du noeud\n »; Créons deux objets : auto a = std::make_shared<Noeud>(); auto b = std::make_shared<Noeud>(); nous obtenons une situation circulaire. a −→ objet A −→ objet B −→ objet A L’objet A possède indirectement une propriété sur B et l’objet B possède une propriété sur A. Lorsque les variables locales a et b disparaissent, les compteurs ne tombent pas à zéro parce ce que chaque objet est encore considéré comme propriétaire de l’autre. Nous obtenons donc un cycle de propriété : Les deux objets peuvent alors rester en mémoire alors que le programme ne peut plus les atteindre depuis ses variables ordinaires et on a une fuite de ressources.
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr Le problème vient de la propriété Il faut bien comprendre la nature du problème précédent et se poser la question si l’objet A doit réellement être propriétaire de B et vice-versa. Dans de nombreuses structures de données, la réponse est non, un objet peut avoir besoin de connaître un autre objet sans pour autant participer à sa propriété. Il faut donc pouvoir exprimer la relation suivante : Je connais cette ressource et je peux éventuellement y accéder, mais je n’en suis pas propriétaire. std::weak_ptr : observer sans posséder Un weak_ptr est associé à une ressource gérée par des shared_ptr, mais il ne participe pas à sa propriété donc il n’augmente pas le nombre de propriétaires. auto p = std::make_shared<int>(42); std::weak_ptr<int> w = p; Nous avons conceptuellement : La flèche en pointillés représente une relation d’observation et non de propriété, le compteur de propriétaires reste égal à 1 et non à 2. Pourquoi un weak_ptr ne peut-il pas être déréférencé directement ? std::weak_ptr<int> w; auto p = std::make_shared<int>(42); À l’intérieur du bloc, p est propriétaire de l’entier, lorsque le bloc se termine, p est détruit, il n’existe alors plus aucun propriétaire et l’entier est détruit. Cependant w existe toujours. Un weak_ptr peut donc désigner une ressource qui a déjà disparu. Pour cette raison, nous ne pouvons pas simplement écrire : comme avec un shared_ptr.
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr Tester l’existence de la ressource Un weak_ptr implémente la fonction expired() qui permet de savoir si la ressource existe encore. std::cout << « La ressource n’existe plus\n »; Mais lorsqu’on souhaite réellement utiliser la ressource, une autre méthode est généralement préférable, c’est la méthode lock(). La fonction w.lock() tente de créer un shared_ptr vers la ressource observée. Si celle-ci existe encore, le shared_ptr obtenu est valide sinon, il est vide. Nous pouvons donc écrire : if (auto p = w.lock()) std::cout << *p << ’\n’; std::cout << « La ressource n’existe plus\n »; Si la ressource existe, lock() crée temporairement un nouveau propriétaire et pendant la durée de vie de ce shared_ptr, la ressource ne peut pas disparaître. • Un weak_ptr n’est pas propriétaire de la ressource qu’il observe. • Pour utiliser cette ressource en toute sécurité, on peut appeler : qui retourne un shared_ptr. • Si la ressource existe encore, ce shared_ptr en garantit la durée de vie pendant son utilisation. Rompre une référence circulaire Revenons à nos deux objets se possédant l’un l’autre, au lieu d’écrire std::shared_ptr<Noeud> autre; dans les deux directions, l’une des relations peut être non propriétaire :
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr std::weak_ptr<Noeud> autre; Le principe général consiste à utiliser shared_ptr pour les relations qui représentent réellement une propriété et weak_ptr pour les relations qui ne font qu’observer un objet ce qui rompt le cycle de propriété. Les compteurs des shared_ptr peuvent revenir à zéro et les objets sont correctement détruits. Les trois modèles de relation Nous disposons maintenant de trois outils permettant d’exprimer des relations différentes entre un programme et une ressource. propriétaire exclusif propriétaire partagé Nous pouvons également résumer leur comportement : observation du contrôle partagé Quel pointeur intelligent choisir ? Le choix ne doit pas commencer par la question : Quel pointeur intelligent vais-je utiliser ? Il faut d’abord se demander : Ai-je réellement besoin d’une allocation dynamique ? Si la réponse est non, un objet automatique ordinaire est souvent préférable : Si une ressource dynamique est nécessaire, la question suivante est : Qui doit posséder cette ressource ? Nous pouvons alors raisonner ainsi : un seul propriétaire plusieurs propriétaires nécessaires observation sans propriété Dans le doute, lorsque la propriété exclusive est possible, unique_ptr constitue généralement le choix naturel. La propriété partagée ne doit être introduite que lorsqu’elle correspond réellement à la structure
Accueil
2026 – C++ Partie III std::shared_ptr et std::weak_ptr std::shared_ptr permet à plusieurs objets de partager la propriété d’une même ressource, cette dernière reste vivante tant qu’au moins un propriétaire existe et lorsque le dernier shared_ptr propriétaire disparaît, la ressource est automatiquement détruite. Cette gestion repose sur un comptage de propriétaires associé à un bloc de contrôle cependant, le comptage de références ne suffit pas à résoudre toutes les situations, dans certains cas des relations circulaires entre shared_ptr peuvent empêcher les compteurs d’atteindre zéro et provoquer une fuite de ressources. std::weak_ptr permet alors d’observer une ressource gérée par shared_ptr sans en devenir propriétaire. Nous pouvons finalement retenir les trois modèles fondamentaux : unique_ptr −→ propriété exclusive shared_ptr −→ propriété partagée weak_ptr −→ observation sans propriété Les pointeurs intelligents ne servent donc pas uniquement à remplacer new et delete mais ils permettent surtout d’exprimer explicitement la relation de propriété entre les objets et les ressources.
Accueil
P4-Chap-05 std::shared_ptr et std::weak_ptr
Accueil
Termes à ajouter au glossaire
Contenu

Inscription

×
Cancel