raw, faible_ptr, unique_ptr, shared_ptr etc… Comment les choisir judicieusement?

33

Il y a beaucoup d'indicateurs en C ++, mais pour être honnête dans environ 5 ans en programmation C ++ (en particulier avec Qt Framework), je n'utilise que l'ancien pointeur brut:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Je sais qu'il y a beaucoup d'autres indicateurs "intelligents":

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Mais je n'ai pas la moindre idée de ce qu'il faut en faire et de ce qu'ils peuvent me proposer en comparaison des indicateurs bruts.

Par exemple, j'ai cet en-tête de classe:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Ceci n’est clairement pas exhaustif, mais pour chacun de ces 3 indicateurs, est-il possible de les laisser "bruts" ou dois-je utiliser quelque chose de plus approprié?

Et dans un deuxième temps, si un employeur lira le code, sera-t-il strict sur le type de pointeurs que j'utilise ou non?

CheshireChild
la source
Ce sujet semble tellement approprié pour SO. c'était en 2008 . Et voici quel type de pointeur dois-je utiliser quand? . Je suis sûr que vous pouvez trouver des correspondances encore meilleures. Ceux - ci étaient juste la première que j'ai vu
sehe
imo celui-ci est à la limite puisqu'il s'agit autant du sens conceptuel / de l'intention de ces classes que des détails techniques de leur comportement et de leur mise en œuvre. Puisque la réponse acceptée s’appuie sur la première, je suis heureux d’avoir la «version PSE» de cette question SO.
Ixrec

Réponses:

70

Un pointeur "brut" n'est pas géré. C'est-à-dire la ligne suivante:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... perdra de la mémoire si un accompagnement deleten'est pas exécuté au bon moment.

auto_ptr

Afin de minimiser ces cas, a std::auto_ptr<>été introduit. En raison des limitations de C ++ antérieures à la norme 2011, il est toutefois très facile auto_ptrde perdre de la mémoire. Cela suffit pour des cas limités, comme celui-ci:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

L'un de ses cas d'utilisation les plus faibles est celui des conteneurs. En effet, si une copie d'un auto_ptr<>est créée et que l'ancienne copie n'est pas soigneusement réinitialisée, le conteneur peut supprimer le pointeur et perdre des données.

unique_ptr

En remplacement, C ++ 11 a introduit std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Un tel unique_ptr<>sera correctement nettoyé, même s'il est passé entre les fonctions. Pour ce faire, il représente sémantiquement la "propriété" du pointeur - le "propriétaire" le nettoie. Cela le rend idéal pour une utilisation dans des conteneurs:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

Contrairement à auto_ptr<>, unique_ptr<>se comporte bien ici, et lors du vectorredimensionnement, aucun des objets ne sera supprimé accidentellement lors de la vectorcopie de son magasin de sauvegarde.

shared_ptr et weak_ptr

unique_ptr<>est utile, bien sûr, mais il existe des cas où vous souhaitez que deux parties de votre base de code puissent faire référence au même objet et copier le pointeur tout en garantissant un nettoyage correct. Par exemple, une arborescence peut ressembler à ceci lorsque vous utilisez std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Dans ce cas, nous pouvons même conserver plusieurs copies d'un nœud racine et l'arborescence sera correctement nettoyée lorsque toutes les copies du nœud racine seront détruites.

Cela fonctionne parce que chacun shared_ptr<>conserve non seulement le pointeur sur l'objet, mais également un nombre de références de tous les shared_ptr<>objets qui font référence au même pointeur. Lorsqu'un nouveau est créé, le nombre augmente. Quand on est détruit, le compte diminue. Lorsque le compte atteint zéro, le pointeur est deleted.

Cela pose donc un problème: les structures à double liaison se retrouvent avec des références circulaires. Disons que nous voulons ajouter un parentpointeur sur notre arbre Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Maintenant, si on enlève un Node, il y a une référence cyclique à cela. Il ne sera jamais deleted car son compte de référence ne sera jamais égal à zéro.

Pour résoudre ce problème, vous utilisez un std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Désormais, les choses fonctionneront correctement et la suppression d'un nœud ne laissera pas de références bloquées au nœud parent. Cependant, il est un peu plus compliqué de marcher dans l’arbre:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

De cette façon, vous pouvez verrouiller une référence sur le nœud et vous avez une garantie raisonnable qu'elle ne disparaîtra pas tant que vous y travaillerez, car vous en conservez une shared_ptr<>.

make_shared et make_unique

Maintenant, il y a quelques problèmes mineurs avec shared_ptr<>et unique_ptr<>cela devrait être résolu. Les deux lignes suivantes ont un problème:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Si thrower()lève une exception, les deux lignes perdront de la mémoire. Et plus que cela, shared_ptr<>maintient le compte de références loin de l’objet qu’il pointe et cela peut signifier une seconde allocation). Ce n'est généralement pas souhaitable.

C ++ 11 fournit std::make_shared<>()et C ++ 14 fournit std::make_unique<>()pour résoudre ce problème:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Maintenant, dans les deux cas, même si thrower()une exception est générée, il n'y aura pas de fuite de mémoire. En prime, il make_shared<>()a la possibilité de créer son compte de référence dans le même espace mémoire que son objet géré, ce qui peut être plus rapide et économiser quelques octets de mémoire, tout en vous offrant une garantie de sécurité exceptionnelle!

Notes sur Qt

Il convient toutefois de noter que Qt, qui doit prendre en charge les compilateurs antérieurs à C ++ 11, dispose de son propre modèle de récupération de place: de nombreux utilisateurs QObjectdisposent d’un mécanisme leur permettant d’être détruits correctement sans que l’utilisateur deleten’en ait besoin .

Je ne sais pas comment QObjectva se comporter quand il sera géré par des pointeurs gérés par C ++ 11, je ne peux donc pas dire que ce shared_ptr<QDialog>soit une bonne idée. Je n'ai pas suffisamment d'expérience avec Qt, mais je pense que Qt5 a été ajusté pour ce cas d'utilisation.

dégraissage
la source
1
@Zilators: Veuillez noter mon commentaire ajouté à propos de Qt. La réponse à votre question sur le point de savoir si les trois pointeurs doivent être gérés dépend de la conformité des objets Qt.
Greyfade
2
"les deux font des attributions séparées pour tenir le pointeur"? Non, unique_ptr n'alloue jamais rien d'extra, seul shared_ptr doit allouer un objet reference-count + allocator. "les deux lignes vont perdre de la mémoire"? non, seulement pourrait, même pas une garantie de mauvais comportement.
Déduplicateur
1
@DuDuplicator: Ma formulation doit avoir été mal définie: il shared_ptrs'agit d'un objet séparé - une allocation distincte - de l' newobjet ed. Ils existent à différents endroits. make_shareda la capacité de les regrouper au même endroit, ce qui améliore entre autres la localisation du cache.
Greyfade
2
@ Greyfade: Nononono. shared_ptrest un objet. Et pour gérer un objet, il doit allouer un objet (reference-count (faible + fort) + destructeur). make_sharedpermet d'allouer cela et l'objet géré en un seul morceau. unique_ptrn'utilise pas ceux-ci, il n'y a donc aucun avantage correspondant, mis à part le fait de s'assurer que l'objet est toujours la propriété du pointeur intelligent. En aparté, on peut avoir un shared_ptrqui possède un objet sous-jacent et représente un nullptr, ou qui ne possède pas et représente un non-nullpointer.
Déduplicateur
1
Je l'ai regardé, et il semble y avoir une confusion générale à propos de ce que shared_ptrfait un : 1. Il partage la propriété d'un objet (représenté par un objet interne alloué dynamiquement ayant un compte de référence faible et fort, ainsi qu'un délet). . 2. Il contient un pointeur. Ces deux parties sont indépendantes. make_uniqueet les make_shareddeux s'assurent que l'objet alloué est placé en toute sécurité dans un pointeur intelligent. De plus, make_sharedpermet d'allouer l'objet de propriété et le pointeur géré ensemble.
Déduplicateur