web-dev-qa-db-fra.com

raw, faiblesse_ptr, unique_ptr, shared_ptr etc ... Comment les choisir judicieusement?

Il y a beaucoup de pointeurs en C++ mais pour être honnête dans environ 5 ans en programmation C++ (spécifiquement avec Qt Framework) j'utilise uniquement l'ancien pointeur brut:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Je sais qu'il y a beaucoup d'autres pointeurs "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 quoi faire avec eux et de ce qu'ils peuvent m'offrir en comparaison des pointeurs 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 

Ce n'est clairement pas exhaustif mais pour chacun de ces 3 pointeurs est-il OK de les laisser "bruts" ou dois-je utiliser quelque chose de plus approprié?

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

35
CheshireChild

Un pointeur "brut" est non géré. Autrement dit, la ligne suivante:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... perdra de la mémoire si un delete qui l'accompagne n'est pas exécuté au bon moment.

auto_ptr

Afin de minimiser ces cas, std::auto_ptr<> a été introduit. Cependant, en raison des limites de C++ avant la norme 2011, il est toujours très facile pour auto_ptr De fuir la mémoire. Il suffit toutefois 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 effectué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 lorsqu'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 lorsque le vector se redimensionne, aucun des objets ne sera accidentellement supprimé tandis que le vector copiera son support boutique.

shared_ptr Et weak_ptr

unique_ptr<> Est utile, bien sûr, mais il y a des cas où vous voulez que deux parties de votre base de code puissent se référer au même objet et copier le pointeur tout en garantissant un nettoyage correct. Par exemple, un arbre pourrait ressembler à ceci, lors de l'utilisation de 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 car chaque shared_ptr<> Conserve non seulement le pointeur vers l'objet, mais également un décompte de référence de tous les objets shared_ptr<> Qui font référence au même pointeur. Lorsqu'un nouveau est créé, le nombre augmente. Quand l'un est détruit, le nombre diminue. Lorsque le nombre 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 pointeur parent à notre arborescence 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 nous supprimons un Node, il y a une référence cyclique. Il ne sera jamais deleted car son nombre de références ne sera jamais nul.

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;
};

Maintenant, les choses fonctionneront correctement et la suppression d'un nœud ne laissera pas de références bloquées au nœud parent. Cela rend la marche de l'arbre un peu plus compliquée, cependant:

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

De cette façon, vous pouvez verrouiller une référence au nœud et vous avez une garantie raisonnable qu'elle ne disparaîtra pas pendant que vous travaillez dessus, car vous vous en tenez à shared_ptr<>.

make_shared Et make_unique

Maintenant, il y a quelques problèmes mineurs avec shared_ptr<> Et unique_ptr<> Qui devraient être résolus. 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 décompte de référence loin de l'objet vers lequel il pointe et ceci peut signifie une deuxième 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() lève une exception, il n'y aura pas de fuite de mémoire. En prime, 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é, qui peut à la fois être plus rapide et économiser quelques octets de mémoire, tout en vous offrant une garantie de sécurité exceptionnelle!

Remarques sur Qt

Il convient de noter, cependant, que Qt, qui doit prendre en charge les compilateurs pré-C++ 11, a son propre modèle de récupération de place: de nombreux QObject ont un mécanisme où ils seront détruits correctement sans avoir besoin de la utilisateur pour delete eux.

Je ne sais pas comment QObjects se comportera lorsqu'il sera géré par des pointeurs gérés C++ 11, donc je ne peux pas dire que shared_ptr<QDialog> Est une bonne idée. Je n'ai pas assez d'expérience avec Qt pour dire avec certitude, mais je croyez que Qt5 a été ajusté pour ce cas d'utilisation.

72
greyfade