Références, unique_ptr, shared_ptr et weak_ptr

Quand utiliser des références ou des pointeurs ? Quand utiliser unique_ptr ou shared_ptr/weak_ptr ? Il n’est pas toujours simple de savoir dans quels contextes utiliser une approche ou une autre. Je vais essayer dans cet article de présenter une démarche simple pour aider à résoudre cette question.

Voir aussi : Références, pointeurs et leurs amis

Le contexte

Dans cet article, je parle de « ressource » au sens large, c’est-à- dire n’importe quel objet (qui sera souvent une capsule RAII). Cette ressource est créée dans un contexte donné et sera utilisée par une classe ou fonction utilisateur. Le propriétaire de cette ressource est chargé de la libérer (en général avec le RAII, quand il est lui-même libéré). Dans le cas de plusieurs propriétaires d’une ressource, le dernier propriétaire est chargé de la libérer.
Selon l’utilisation qui est faite de la ressource, plusieurs cas peuvent se présenter. On peut distinguer deux critères influençant la stratégie a adopter :

  • Est-ce que cela a un sens que la variable soit sémantiquement nulle ?
  • Est-ce que l’utilisateur de la ressource devient propriétaire ou non de celle-ci ?

Il faut préciser ce que signifie « sémantiquement nulle » et « être propriétaire ».

Variable pouvant être nulle

Pour comprendre le premier point, prenons l’exemple abordé dans le cours C++ de OpenClassRoom, un personnage qui a un nom et peut avoir une arme. On peut donc écrire :

Personnage p1("toto");
Personnage p2("titi", Pistolet);

Dans cet exemple, un personnage doit avoir un nom (au pire, une chaîne vide). Si ce n’est pas le cas (par exemple, si l’allocation de la mémoire pour créer la chaîne de caractères a échoué), alors il s’agit d’une erreur d’exécution, qui doit être réglée par le lancement d’une exception. Cela n’a pas de sens, pour le nom de la classe Personnage, d’être nulle.

Au contraire, un personnage peut ne pas avoir d’arme. Ne pas avoir d’arme a un sens pour la classe Personnage, ce n’est pas une erreur d’exécution si c’est le cas. Il faut dans ce cas systématiquement vérifier qu’un personnage a une arme avant d’essayer de l’utiliser. La variable peut être sémantiquement nulle.

Propriétaire d’un objet

La notion de propriétaire d’un objet est un peu plus complexe à comprendre. Restons dans l’exemple d’une classe Personnage qui peut posséder une arme. Et imaginons la situation où il achète une arme dans un magasin. La classe Magasin possède une liste d’objets (des armes) et la classe Personnage prend l’un de ses objets et l’utilise comme arme. On peut avoir deux situations.

Dans le premier cas, toutes les armes portées par les personnages sont identiques à celle qui se trouve dans la classe Magasin (toutes les caractéristiques de l’arme sont les mêmes). Dans ce cas, il n’est pas nécessaire que chaque personnage duplique l’objet en mémoire pour l’utiliser, il suffit qu’il puisse accéder au même objet en mémoire que le magasin.

Aucun personnage ne peut détruire un objet en mémoire correspondant à une arme en magasin (sinon, cela signifierait qu’un personnage peut détruire l’objet d’un autre personnage), seul le magasin peut détruire des objets. Dans ce cas de figure, le propriétaire des objets Arme est la classe Magasin, elle est responsable de sa durée de vie. La classe utilisatrice Personnage ne devient pas propriétaire.

L’autre situation est lorsque chaque personnage peut modifier son arme. Par exemple, il peut l’affûter et gagner un point de dégât ou alors elle peut s’user et faire moins de dégâts. Dans ce cas, chaque personnage possède une copie de l’arme qui se trouve en magasin et il n’y a pas deux personnages qui possèdent exactement la même arme. Le magasin est responsable de supprimer les objets en mémoire qu’il possède, mais pas des copies faites par la classe Personnage. Celle-ci est responsable de ses objets, elle en est propriétaire.
Cela nous donne donc quatre cas d’utilisation possible :

La ressource ne peut pas être nulle La ressource peut être nulle
L’utilisateur ne devient pas propriétaire référence shared_ptr et weak_ptr
L’utilisateur devient propriétaire déplacement unique_ptr

Déplacement

Dans le cas où l’utilisateur devient propriétaire de la ressource et que celle-ci ne peut pas être nulle, il faut utiliser la sémantique de déplacement. (Avant l’introduction de la sémantique de déplacement dans le C++11, on utilisait un pointeur dans ce cas, c’était le seul moyen de prendre la responsabilité de la ressource).

class RessourceUser {
    Ressource r_;
public:
    RessourceUser(Ressource && r) : 
        r_ { std::move(r) } {}
};
Ressource r;
RessourceUser u(std::move(r));

Lors de la création de u, r devient invalide, la ressource est « consommée » par u. u est l’unique propriétaire de la ressource, si u est détruit, la ressource est détruite aussi.

Référence

Dans le cas où l’utilisateur ne devient pas propriétaire de la ressource et que celle-ci ne peut pas être nulle, il faut utiliser une référence (ou un std::reference_wrapper si on souhaite placer la ressource dans un conteneur).

class RessourceUser {
    Ressource & r_;
public:
    RessourceUser(Ressource & r) : r_ { r } {}
};
Ressource r;
RessourceUser u(r);

Dans ce cas, u n’est pas propriétaire, la ressource ne sera pas libérée lorsque u est détruit.
Remarque : certains critiquent les références en disant qu’elles n’apportent aucune garantie sur le fait que la ressource ne peut être nulle. En effet, il est possible d’écrire :

Ressource * r = new Ressource {};
Ressource & ref = *r;
delete r;
use(ref); // erreur, ref est invalide

Ce n’est bien sûr pas faux dans l’absolu, mais cela est la conséquence d’une très mauvaise pratique. En effet, on convertit une variable pouvant être sémantiquement nulle en une variable qui ne peut pas l’être. Le non-respect des sémantiques est quelque chose qui sera toujours dangereux.

Unique_ptr

Dans le cas où l’utilisateur devient propriétaire de la ressource et que celle-ci peut être nulle, il faut utiliser un std::unique_ptr.

class RessourceUser {
    std::unique_ptr r_;
public:
    RessourceUser(std::unique_ptr r) : 
    r_ { std::move(r) } {}
};
auto r = std::make_unique();
RessourceUser u(std::move(r));

Dans ce cas également, r devient invalide et la ressource est détruite lorsque u est détruit.

Shared_ptr et weak_ptr

Dans le dernier cas, l’utilisateur ne devient pas propriétaire de la ressource et que celle-ci peut être nulle, il faut utiliser des std::shared_ptr et std::weak_ptr.

On pourrait avoir l’idée d’utiliser un std::unique_ptr dans ce cas et le partager en utilisant une référence (ou un pointeur nu) :

class RessourceUser {
    std::unique_ptr & r_;
public:
    RessourceUser(std::unique_ptr & r) : r_ { r } {}
    // ou RessourceUser(Ressource * r);
};
auto r = std::make_unique();
RessourceUser u(r);

Cette approche peut être dangereuse. Par défaut, il est raisonnable de considérer que l’on est dans une situation de multithread (soit parce que l’on n’a pas l’information si l’on est en multithread ou non, soit parce que l’on veut que notre code soit évolutif et que même s’il n’est pas utilisé en multithread pour l’instant, il pourra l’être dans le futur).

Dans cette situation, rien n’interdit au thread propriétaire de la ressource de la libérer pendant que u l’utilise, ce qui serait catastrophique. Cela explique pourquoi il n’y a pas d’équivalent de weak_ptr pour unique_ptr. En effet, pour que u puisse utiliser la ressource, il doit devenir temporairement propriétaire de cette ressource (cf weak_ptr, il n’est pas possible d’utiliser directement un weak_ptr, il faut d’abord le promouvoir en shared_ptr).

Il faut dans ce cas utiliser un shared_ptr sur la ressource (propriétaire principal) et la partager en utilisant weak_ptr (observateur). Au moment de l’utiliser, il faut devenir propriétaire temporaire de la ressource (promotion de weak_ptr en shared_ptr).

class RessourceUser {
    std::weak_ptr r_;
public:
    RessourceUser(std::weak_ptr r) : 
        r_ { std::move(r) } {}
    void use() {
        auto local_r = r_.lock());
        // utilisation de local_r
     } // libération de local_r
};
auto r = std::make_shared();
RessourceUser u(r);

Il est également possible d’utiliser un shared_ptr en paramètre, mais dans ce cas, il y a plusieurs « vrais » propriétaires de la ressource. En général, cette situation est problématique (on perd le contrôle de la durée de vie de la ressource) et évitable.

Conclusion

Cet article est une tentative de simplifier les cas d’utilisation. Si j’ai oublié des cas, n’hésitez pas à me le signaler.

Mises à jour

  • 15/09/2014 : corrections des remarques faites par Flob90. Ajout d’explications sur la notion de propriétaire ou non.

Complément : Différence entre std::unique_ptr et std::shared_ptr.(Quand les utiliser ?)

Publicités

2 commentaires sur « Références, unique_ptr, shared_ptr et weak_ptr »

  1. Bonjour,

    Etant donné tes exemples, tu considères ta ressource comme copiable, je trouve que c’est un peu osé comme supposition. Si j’enlève cette supposition, ton premier code devient :

    class RessourceUser {
    Ressource r_;
    public:
    RessourceUser(Ressource&& r) : r_ { std::move(r) } {}
    };

    Ressource r;
    RessourceUser u(std::move(r));
    //RessourceUser a pris la ressource r, on ne doit plus l’utiliser ici

    Ton code sur unique_ptr ne compile pas, il manque un move lors du passage de r : u(std::move(r)). Maintenant sur le fond, il se passe quoi pour ma ressource r si le constructeur de RessourceUser lance une exception ? Avec ton code actuel je perds totalement ma ressource, j’aurais tendance à préférer :

    RessourceUser(std::unique_ptr&& r) :
    r_ { std::move(r) } {}

    Avec ce code on perd toujours la ressource, mais il est possible à la personne qui écrit cette classe d’attraper les éventuelles exceptions et de rétablir la ressource :

    try
    r_ { std::move(r) }
    { }
    catch(…)
    {
    r=std::move(r_);
    throw;
    }

    Conférant ainsi une résistance forte aux exceptions : si le constructeur échoue, je me retrouve à l’état avant son appel.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s