Quel type de pointeur dois-je utiliser quand?

228

Ok, donc la dernière fois que j'ai écrit C ++ pour gagner ma vie, std::auto_ptrc'était toute la lib std disponible, et boost::shared_ptrc'était à la mode. Je n'ai jamais vraiment examiné les autres types de pointeurs intelligents fournis. Je comprends que C ++ 11 fournit maintenant certains des types de boost proposés, mais pas tous.

Quelqu'un a-t-il donc un algorithme simple pour déterminer quand utiliser quel pointeur intelligent? Incluant de préférence des conseils concernant les pointeurs stupides (pointeurs bruts comme T*) et le reste des pointeurs intelligents boost. (Quelque chose comme ça serait génial).

sbi
la source
Voir aussi std :: auto_ptr à std :: unique_ptr
Martin York
1
J'espère vraiment que quelqu'un arrivera avec un joli organigramme pratique comme cet organigramme de sélection STL .
Alok Save
1
@Als: Oh, c'est en effet une belle! Je l'ai FAQisé.
sbi
6
@Deduplicator Ce n'est même pas près d'être un doublon. La question liée indique "Quand dois-je utiliser un pointeur intelligent" et cette question est "Quand dois-je utiliser ces pointeurs intelligents?" c'est-à-dire que celui-ci catégorise les différentes utilisations des pointeurs intelligents standard. La question liée ne fait pas cela. La différence est apparemment petite mais elle est grande.
Rapptz

Réponses:

183

Propriété partagée:
Le shared_ptret weak_ptrla norme adoptée sont à peu près les mêmes que leurs homologues Boost . Utilisez-les lorsque vous avez besoin de partager une ressource et que vous ne savez pas laquelle sera la dernière en vie. Utilisez weak_ptrpour observer la ressource partagée sans influencer sa durée de vie, pas pour rompre les cycles. Les cycles avec shared_ptrne devraient normalement pas se produire - deux ressources ne peuvent pas se posséder.

Notez que Boost propose en outre shared_array, ce qui pourrait être une alternative appropriée à shared_ptr<std::vector<T> const>.

Ensuite, les offres Boost intrusive_ptr, qui sont une solution légère si votre ressource propose déjà une gestion comptée par référence et que vous souhaitez l'adapter au principe RAII. Celui-ci n'a pas été adopté par la norme.

Propriété unique:
Boost possède également un scoped_ptr, qui n'est pas copiable et pour lequel vous ne pouvez pas spécifier de suppresseur. std::unique_ptrest boost::scoped_ptrsous stéroïdes et devrait être votre choix par défaut lorsque vous avez besoin d'un pointeur intelligent . Il vous permet de spécifier un suppresseur dans ses arguments de modèle et est mobile , contrairement à boost::scoped_ptr. Il est également entièrement utilisable dans les conteneurs STL tant que vous n'utilisez pas d'opérations qui nécessitent des types copiables (évidemment).

Notez à nouveau, que Boost a une version de tableau:, scoped_arrayque la norme a unifié en exigeant std::unique_ptr<T[]>une spécialisation partielle qui fera delete[]pointer le pointeur au lieu de deletele faire (avec le default_deleter). std::unique_ptr<T[]>propose également operator[]au lieu de operator*et operator->.

Notez que std::auto_ptrc'est toujours dans la norme, mais c'est obsolète . §D.10 [depr.auto.ptr]

Le modèle de classe auto_ptrest obsolète. [ Remarque: Le modèle de classe unique_ptr(20.7.1) fournit une meilleure solution. —Fin note ]

Pas de propriété:
utilisez des pointeurs muets (pointeurs bruts) ou des références pour les références non propriétaires aux ressources et lorsque vous savez que la ressource survivra à l'objet / portée de référence. Préférez les références et utilisez des pointeurs bruts lorsque vous avez besoin de nullité ou de réinitialisation.

Si vous voulez une référence non propriétaire à une ressource, mais vous ne savez pas si la ressource survivra à l'objet qui la référence, emballez la ressource dans un shared_ptret utilisez un weak_ptr- vous pouvez tester si le parent shared_ptrest vivant lock, ce qui retourne un shared_ptrqui est non nul si la ressource existe toujours. Si vous souhaitez tester si la ressource est morte, utilisez expired. Les deux peuvent sembler similaires, mais sont très différents face à une exécution simultanée, car ils expiredgarantissent uniquement sa valeur de retour pour cette seule instruction. Un test apparemment innocent comme

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

est une condition de concurrence potentielle.

Xeo
la source
1
Dans le cas d'aucune propriété, vous devriez probablement préférer les références aux pointeurs à moins que vous n'ayez pas besoin de propriété et de réinitialisabilité là où les références ne le coupent pas, même alors vous voudrez peut-être envisager de réécrire l'objet d'origine pour être un shared_ptret le pointeur non propriétaire pour être a weak_ptr...
David Rodríguez - dribeas
2
Je ne parlais pas d'une référence au pointeur , mais plutôt d'une référence au lieu d'un pointeur. S'il n'y a pas de propriété, sauf si vous avez besoin d'une réinitialisation (ou d'une nullité, mais que la nullité sans pouvoir être réinitialisée serait assez limitée), vous pouvez utiliser une référence simple plutôt qu'un pointeur en premier lieu.
David Rodríguez - dribeas
1
@David: Ah, je vois. :) Oui, les références ne sont pas mauvaises pour ça, personnellement je les préfère aussi dans de tels cas. Je vais les ajouter.
Xeo
1
@Xeo: shared_array<T>est une alternative à shared_ptr<T[]>ne pas faire shared_ptr<vector<T>>: il ne peut pas grandir.
R. Martinho Fernandes
1
@GregroyCurrie: C'est ... exactement ce que j'ai écrit? J'ai dit que c'était un exemple d'une condition de course potentielle.
Xeo
127

Décider quel pointeur intelligent utiliser est une question de propriété . En ce qui concerne la gestion des ressources, l'objet A possède l' objet B s'il contrôle la durée de vie de l'objet B. Par exemple, les variables membres appartiennent à leurs objets respectifs car la durée de vie des variables membres est liée à la durée de vie de l'objet. Vous choisissez des pointeurs intelligents en fonction de la propriété de l'objet.

Notez que la propriété d'un système logiciel est distincte de la propriété comme nous le penserions en dehors du logiciel. Par exemple, une personne peut «posséder» sa maison, mais cela ne signifie pas nécessairement qu'un Personobjet a le contrôle sur la durée de vie d'un Houseobjet. La confrontation de ces concepts du monde réel avec des concepts logiciels est une façon infaillible de vous programmer dans un trou.


Si vous êtes l'unique propriétaire de l'objet, utilisez std::unique_ptr<T>.

Si vous avez partagé la propriété de l'objet ...
- S'il n'y a pas de cycle de propriété, utilisez std::shared_ptr<T>.
- S'il y a des cycles, définir une "direction" et utiliser std::shared_ptr<T>dans un sens et std::weak_ptr<T>dans l'autre.

Si l'objet vous appartient, mais qu'il est possible de ne pas avoir de propriétaire, utilisez des pointeurs normaux T*(par exemple, des pointeurs parents).

Si l'objet vous appartient (ou a une existence garantie d'une autre manière), utilisez des références T&.


Avertissement: soyez conscient des coûts des pointeurs intelligents. Dans les environnements à mémoire ou performances limitées, il peut être avantageux d'utiliser simplement des pointeurs normaux avec un schéma plus manuel pour gérer la mémoire.

Les coûts:

  • Si vous avez un suppresseur personnalisé (par exemple, vous utilisez des pools d'allocation), cela entraînera des frais généraux par pointeur qui peuvent être facilement évités par une suppression manuelle.
  • std::shared_ptra la surcharge d'un incrément de comptage de référence lors de la copie, plus un décrément lors de la destruction suivi d'une vérification de 0 comptage avec suppression de l'objet retenu. Selon l'implémentation, cela peut alourdir votre code et entraîner des problèmes de performances.
  • Compiler le temps. Comme avec tous les modèles, les pointeurs intelligents contribuent négativement aux temps de compilation.

Exemples:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Un arbre binaire ne possède pas son parent, mais l'existence d'un arbre implique l'existence de son parent (ou nullptrpour root), de sorte qu'il utilise un pointeur normal. Un arbre binaire (avec une sémantique de valeur) a la propriété exclusive de ses enfants, donc ce sont std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Ici, le nœud de liste possède ses listes suivante et précédente, nous définissons donc une direction et utilisons shared_ptrpour suivant et weak_ptrpour prev pour rompre le cycle.

Peter Alexander
la source
3
Pour l'exemple de l'arbre binaire, certaines personnes suggèrent d'utiliser shared_ptr<BinaryTree>pour les enfants et weak_ptr<BinaryTree>pour la relation parentale.
David Rodríguez - dribeas
@ DavidRodríguez-dribeas: Cela dépend si l'Arbre a une sémantique de valeur ou non. Si les gens vont référencer votre arbre en externe même une fois que l'arbre source est détruit, alors oui, la combinaison de pointeurs partagés / faibles serait la meilleure.
Peter Alexander
Si un objet vous appartient et est garanti d'exister, alors pourquoi pas une référence.
Martin York
1
Si vous utilisez la référence, vous ne pouvez jamais changer le parent, ce qui peut ou non gêner la conception. Pour équilibrer les arbres, cela gênerait.
Mooing Duck
3
+1 mais vous devez ajouter une définition de "propriété" sur la première ligne. Je me retrouve souvent à déclarer clairement qu'il s'agit de la vie et de la mort de l'objet, et non de la propriété dans un sens plus spécifique au domaine.
Klaim
19

Utilisez unique_ptr<T>tout le temps sauf lorsque vous avez besoin d'un comptage de référence, auquel cas utilisez shared_ptr<T>(et dans de très rares cas, weak_ptr<T>pour éviter les cycles de référence). Dans presque tous les cas, la propriété unique transférable est très bien.

Pointeurs bruts: bon uniquement si vous avez besoin de retours covariants, pointage non propriétaire qui peut se produire. Ils ne sont pas terriblement utiles autrement.

Pointeurs de tableau: unique_ptra une spécialisation pour T[]laquelle appelle automatiquement delete[]le résultat, vous pouvez donc le faire en toute sécurité unique_ptr<int[]> p(new int[42]);par exemple. shared_ptrvous auriez toujours besoin d'un suppresseur personnalisé, mais vous n'auriez pas besoin d'un pointeur de tableau partagé ou unique spécialisé. Bien sûr, de telles choses sont généralement mieux remplacées par de std::vectortoute façon. Malheureusement, shared_ptrne fournit pas de fonction d'accès au tableau, vous devrez donc toujours appeler manuellement get(), mais unique_ptr<T[]>fournit à la operator[]place de operator*et operator->. Dans tous les cas, vous devez vous contrôler vous-même. Cela rend shared_ptrlégèrement moins convivial, bien que sans doute l'avantage générique et qu'aucune dépendance Boost ne fasse unique_ptret shared_ptrles gagnants à nouveau.

Pointeurs étendus: rendus non pertinents par unique_ptr, tout comme auto_ptr.

Il n'y a vraiment plus rien. En C ++ 03 sans sémantique de déplacement, cette situation était très compliquée, mais en C ++ 11, le conseil est très simple.

Il existe encore des utilisations pour d'autres pointeurs intelligents, comme intrusive_ptrou interprocess_ptr. Cependant, ils sont très niches et complètement inutiles dans le cas général.

Chiot
la source
En outre, des pointeurs bruts pour l'itération. Et pour les tampons de paramètres de sortie, où le tampon appartient à l'appelant.
Ben Voigt
Hmm, la façon dont je lis cela, ce sont des situations qui sont à la fois un retour covariant et un non-propriétaire. Une réécriture pourrait être bonne si vous vouliez dire l'union plutôt que l'intersection. Je dirais également que l'itération mérite également une mention spéciale.
Ben Voigt
2
std::unique_ptr<T[]>fournit operator[]au lieu de operator*et operator->. Il est vrai que vous devez toujours vérifier vous-même les limites.
Xeo
8

Cas d'utilisation unique_ptr:

  • Méthodes d'usine
  • Membres qui sont des pointeurs (bouton inclus)
  • Stockage de pointeurs dans les contenants stl (pour éviter les mouvements)
  • Utilisation de grands objets dynamiques locaux

Cas d'utilisation shared_ptr:

  • Partage d'objets entre des threads
  • Partage d'objets en général

Cas d'utilisation weak_ptr:

  • Grande carte qui sert de référence générale (par exemple, une carte de toutes les sockets ouvertes)

N'hésitez pas à modifier et ajouter plus

Lalaland
la source
En fait, j'aime davantage votre réponse lorsque vous donnez des scénarios.
Nicholas Humphrey