web-dev-qa-db-fra.com

Quand utiliser shared_ptr et quand utiliser des pointeurs bruts?

class B;

class A
{
public:
    A ()
        : m_b(new B())
    {
    }

    shared_ptr<B> GimmeB ()
    {
        return m_b;
    }

private:
    shared_ptr<B> m_b;
};

Disons que B est une classe qui, sémantiquement, ne devrait pas exister en dehors de la durée de vie de A, c'est-à-dire qu'il n'a absolument aucun sens que B existe par lui-même. Si GimmeB retourne un shared_ptr<B> ou un B*?

En général, est-ce une bonne pratique d'éviter complètement d'utiliser des pointeurs bruts dans du code C++, au lieu de pointeurs intelligents?

Je suis d'avis que shared_ptr ne doit être utilisé qu'en cas de transfert ou de partage explicite de la propriété, ce qui, je pense, est assez rare en dehors des cas où une fonction alloue de la mémoire, la remplit avec des données et la renvoie, et il existe une compréhension entre l'appelant et l'appelé que le premier est désormais "responsable" de ces données.

71
TripShock

Votre analyse est tout à fait correcte, je pense. Dans cette situation, je retournerais également un _ B*, ou même un [const] B& si l'objet est garanti de ne jamais être nul.

Ayant eu le temps de parcourir les pointeurs intelligents, je suis arrivé à quelques lignes directrices qui me disent quoi faire dans de nombreux cas:

  • Si vous renvoyez un objet dont la durée de vie doit être gérée par l'appelant, retournez std::unique_ptr. L'appelant peut l'assigner à un std::shared_ptr s'il le veut.
  • Retour std::shared_ptr est en fait assez rare, et quand cela a du sens, c'est généralement évident: vous indiquez à l'appelant qu'il prolongera la durée de vie de l'objet pointé au-delà de la durée de vie de l'objet qui maintenait à l'origine la ressource. Le retour de pointeurs partagés depuis les usines ne fait pas exception: vous devez le faire par exemple. lorsque vous utilisez std::enable_shared_from_this.
  • Vous avez très rarement besoin de std::weak_ptr, sauf lorsque vous souhaitez donner un sens à la méthode lock. Cela a certaines utilisations, mais elles sont rares. Dans votre exemple, si la durée de vie de l'objet A n'était pas déterministe du point de vue de l'appelant, cela aurait dû être pris en compte.
  • Si vous renvoyez une référence à un objet existant dont la durée de vie ne peut pas être contrôlée par l'appelant, renvoyez un pointeur nu ou une référence. Ce faisant, vous dites à l'appelant qu'un objet existe et qu'elle n'a pas à prendre soin de sa durée de vie. Vous devez renvoyer une référence si vous n'utilisez pas la valeur nullptr.
64
Alexandre C.

La question "quand dois-je utiliser shared_ptr et quand dois-je utiliser des pointeurs bruts? "a une réponse très simple:

  • Utilisez des pointeurs bruts lorsque vous ne souhaitez pas associer de propriété au pointeur. Ce travail peut également souvent être effectué avec des références. Les pointeurs bruts peuvent également être utilisés dans certains codes de bas niveau (comme pour implémenter des pointeurs intelligents ou implémenter des conteneurs).
  • Utilisation unique_ptr ou scope_ptr lorsque vous souhaitez une propriété unique de l'objet. Il s'agit de l'option la plus utile et doit être utilisée dans la plupart des cas. La propriété unique peut également être exprimée en créant simplement un objet directement, plutôt qu'en utilisant un pointeur (c'est encore mieux que d'utiliser un unique_ptr, si cela est possible).
  • Utilisation shared_ptr ou intrusive_ptr lorsque vous souhaitez partager la propriété du pointeur. Cela peut être déroutant et inefficace, et ce n'est souvent pas une bonne option. La propriété partagée peut être utile dans certaines conceptions complexes, mais devrait être évitée en général, car elle conduit à un code difficile à comprendre.

shared_ptrs effectuent une tâche totalement différente des pointeurs bruts, et ni shared_ptrs ni les pointeurs bruts sont la meilleure option pour la majorité du code.

25
Mankarse

Voici une bonne règle générale:

  • Lorsqu'il n'y a pas de transfert ou de propriété partagée, les références ou les pointeurs simples sont suffisants. (Les pointeurs simples sont plus flexibles que les références.)
  • Lorsqu'il y a transfert de propriété mais pas de propriété partagée, alors std::unique_ptr<> est un bon choix. C'est souvent le cas avec les fonctions d'usine.
  • Lorsqu'il existe une propriété partagée, c'est un bon cas d'utilisation pour std::shared_ptr<> ou boost::intrusive_ptr<>.

Il est préférable d'éviter la propriété partagée, en partie parce qu'ils sont les plus chers en termes de copie et std::shared_ptr<> prend le double du stockage d'un pointeur simple, mais, surtout, parce qu'ils sont propices à des conceptions pauvres où il n'y a pas de propriétaires clairs, ce qui, à son tour, conduit à une boule de poils d'objets qui ne peuvent pas être détruits car ils contiennent des pointeurs partagés les uns aux autres.

La meilleure conception est celle où la propriété claire est établie et hiérarchique, de sorte que, idéalement, aucun pointeur intelligent n'est nécessaire. Par exemple, s'il existe une fabrique qui crée des objets uniques ou renvoie des objets existants, il est logique que la fabrique possède les objets qu'elle crée et les conserve simplement par valeur dans un conteneur associatif (tel que std::unordered_map), afin qu'il puisse renvoyer des pointeurs ou des références simples à ses utilisateurs. Cette fabrique doit avoir une durée de vie qui commence avant son premier utilisateur et se termine après son dernier utilisateur (la propriété hiérarchique), afin que les utilisateurs ne puissent pas avoir de pointeur vers un objet déjà détruit.

10
Maxim Egorushkin

Si vous ne voulez pas que l'appelé de GimmeB () puisse prolonger la durée de vie du pointeur en conservant une copie du ptr après la mort de l'instance de A, alors vous ne devriez certainement pas retourner un shared_ptr.

Si l'appelé n'est pas censé conserver le pointeur renvoyé pendant de longues périodes, c'est-à-dire qu'il n'y a aucun risque d'expiration de la durée de vie de A avant le pointeur, alors le pointeur brut serait mieux. Mais même un meilleur choix consiste simplement à utiliser une référence, à moins qu'il n'y ait une bonne raison d'utiliser un pointeur brut réel.

Et enfin, dans le cas où le pointeur renvoyé peut exister après l'expiration de la durée de vie de l'instance A, mais vous ne voulez pas que le pointeur lui-même prolonge la durée de vie du B, vous pouvez alors renvoyer un faiblesse_ptr, que vous pouvez utiliser pour tester si elle existe toujours.

L'essentiel est qu'il existe généralement une meilleure solution que d'utiliser un pointeur brut.

6
reko_t

Je suis d'accord avec votre opinion que shared_ptr est mieux utilisé en cas de partage explicite des ressources, mais il existe d'autres types de pointeurs intelligents.

Dans votre cas précis: pourquoi ne pas renvoyer une référence?

Un pointeur suggère que les données peuvent être nulles, mais ici, il y aura toujours un B dans votre A, donc il ne sera jamais nul. La référence affirme ce comportement.

Cela étant dit, j'ai vu des gens préconiser l'utilisation de shared_ptr même dans des environnements non partagés, et donnant weak_ptr gère, avec l'idée de "sécuriser" l'application et d'éviter les pointeurs périmés. Malheureusement, puisque vous pouvez récupérer un shared_ptr du weak_ptr (et c'est le seul moyen de réellement manipuler les données), il s'agit toujours d'une propriété partagée même si elle n'était pas censée l'être.

Remarque: il y a un bug subtil avec shared_ptr, une copie de A partagera le même B que l'original par défaut, sauf si vous écrivez explicitement un constructeur de copie et un opérateur d'affectation de copie. Et bien sûr, vous n'utiliseriez pas de pointeur brut dans A pour contenir un B, voulez-vous :)?


Bien sûr, une autre question est de savoir si vous devez réellement le faire. L'un des principes d'une bonne conception est l'encapsulation . Pour réaliser l'encapsulation:

Vous ne devez pas retourner les poignées à vos internes (voir Loi de Déméter ).

alors peut-être que la vraie réponse à votre question est qu'au lieu de donner une référence ou un pointeur à B, il ne devrait être modifié que via l'interface de A.

3
Matthieu M.

En règle générale, j'éviterais d'utiliser des pointeurs bruts autant que possible, car ils ont une signification très ambiguë - vous devrez peut-être désallouer la pointe, mais peut-être pas, et seule la documentation lue et écrite par l'homme vous indique quel est le cas. Et la documentation est toujours mauvaise, obsolète ou mal comprise.

Si la propriété est un problème, utilisez un pointeur intelligent. Sinon, j'utiliserais une référence si possible.

2
thiton
  1. Vous allouez B à la construction de A.
  2. Vous dites que B ne devrait pas persister en dehors de la vie.
    Ces deux éléments indiquent que B est un membre de A et vient de renvoyer un accesseur de référence. Êtes-vous en train de trop concevoir cela?
2
Ricibob

J'ai trouvé que les directives de base C++ donnent des conseils très utiles pour cette question:

Utiliser un pointeur brut (T *) ou un pointeur plus intelligent dépend de qui possède l'objet (dont la responsabilité de libérer la mémoire de l'obj).

posséder :

smart pointer, owner<T*>

pas propriétaire:

T*, T&, span<>

le propriétaire <>, span <> est défini dans la bibliothèque Microsoft GSL

voici les règles de base:

1) N'utilisez jamais de pointeur brut (ou pas de types propres) pour transmettre la propriété

2) le pointeur intelligent ne doit être utilisé que lorsque la sémantique de propriété est prévue

3) T * ou le propriétaire désigne un objet individuel (uniquement)

4) utilisez vector/array/span pour array

5) À mon sens, shared_ptr est généralement utilisé lorsque vous ne savez pas qui publiera l'obj, par exemple, un obj est utilisé par plusieurs threads

2
camino

Il est recommandé d'éviter d'utiliser des pointeurs bruts, mais vous ne pouvez pas tout remplacer par shared_ptr. Dans l'exemple, les utilisateurs de votre classe supposeront qu'il est acceptable de prolonger la durée de vie de B au-delà de celle de A et peuvent décider de conserver l'objet B retourné pendant un certain temps pour leurs propres raisons. Vous devez renvoyer un weak_ptr, ou, si B ne peut absolument pas exister lorsque A est détruit, une référence à B ou simplement un pointeur brut.

1
hamstergene

Quand vous dites: "Disons que B est une classe qui ne devrait sémantiquement pas exister en dehors de la durée de vie de A"

Cela me dit que B ne devrait pas logiquement exister sans A, mais qu'en est-il physiquement? Si vous pouvez être sûr que personne n'essaiera d'utiliser un * B après les dtors A, alors peut-être qu'un pointeur brut conviendra. Sinon, un pointeur plus intelligent peut être approprié.

Lorsque les clients ont un pointeur direct sur A, vous devez avoir confiance qu'ils le géreront de manière appropriée; ne pas essayer de le détailler, etc.

0
seand