2010-02-03 27 views
2

J'ai eu récemment le bug de mémoire suivante, qui est facile à repérer, mais peut être plus difficile à détecter dans le code plus complexe:Pour obtenir le comptage des références, dois-je encombrer mes API avec shared_ptr?

class Foo : public IFoo { 
    const Bar& bar_; 
public: 
    Foo(const Bar& bar) : bar_(bar) { 
    } 
    void test() { 
    // access bar_ here 
    } 
}; 

int baz() { 
    IFoo* foo = NULL; 
    if(whatever) { 
    Bar bar; 
    foo = new Foo(bar); 
    } 
    else { 
    // create other foo's 
    } 
    foo->test(); // segmentation fault 
} 

Le bug est que Bar immédiatement est hors de portée, est détruit puis utilisé dans foo->test(). Une solution consiste à créer Bar sur le tas, en utilisant Bar* bar = new Bar(). Cependant, je n'aime pas faire cela parce que je devrais garder le pointeur Bar* bar au niveau supérieur pour que je puisse y accéder et delete à la fin, même si Bar est quelque chose qui est spécifique à ce bloc de code particulier if(whatever){}.

Une autre solution est boost::shared_ptr<Bar>, mais je ne peux pas écrire ceci:

if(whatever) { 
    boost::shared_ptr<Bar> bar(new Bar()); 
    foo = new Foo(*bar); 
    } 

depuis le shared_ptr est hors de portée immédiatement, aussi, détruire l'objet contenu.

Donc en bref, pour se débarrasser de ce problème, je dois utiliser shared_ptr partout, en Foo comme variable membre, dans le constructeur de Foo, etc. Pour éliminer ces problèmes en général, toutes mes API etc doivent utiliser shared_ptr, ce qui est un peu moche. Mais, est-ce la bonne chose à faire? Jusqu'à présent, je l'ai utilisé parfois pour créer des objets comptés par référence, mais j'ai gardé mes API propres de shared_ptr. Comment gérez-vous ce problème que, une fois que vous utilisez shared_ptr vous devez l'utiliser partout?

(De plus, si vous utilisez ces pointeurs de référence compté, vous devez commencer à se soucier si vous voulez vraiment shared_ptr ou plutôt weak_ptr etc.)

Et, qu'est-ce que j'utiliser comme un équivalent à Foo(const Bar& bar)? Foo(const shared_ptr<const Bar> bar)? Une autre option, bien sûr, est d'ajouter vous-même le comptage des références à l'intérieur du Bar et d'autres objets, en utilisant pimpl et vos propres compteurs, mais cela devient en général trop fastidieux.

Répondre

10

En fait, je utiliser shared_ptr partout ... Il y a plusieurs façons de le rendre moins encombré. Une convention que j'utilise est typedefs pour chaque classe définie:

class AClass { 
public: 
    typedef boost::shared_ptr<AClass> Ptr; 
    typedef boost::weak_ptr<AClass> Ref; 
    //... 
}; 

rend le code beaucoup plus lisible :)

En ce qui concerne votre problème, rappelez-vous que vous pouvez passer la barre par pointeur - vous auriez pour se rappeler de l'allouer sur le tas si.

+0

Merci! J'aime la solution typedef. Et je ne savais pas que 'weak_ptr' serait vu comme une référence? Intéressant. (A propos de l'allocation de tas, j'ai discuté de cela dans ma question :-) – Frank

+0

@dehmann, weak_ptr comme référence est ma propre convention, mais au moins pour moi cela a du sens - weak_ptr ne possède pas d'objet, juste des références à celui-ci. –

+0

Je viens de "typedef" "boost :: shared_ptr" globalement dans "sp" (via l'héritage, car les typedefs ne supportent pas le template). beaucoup moins de travail –

2

Il serait préférable de ne pas passer la barre byref dans Foo (en d'autres termes, changer le constructeur de Foos) ou de le copier afin de conserver une copie dans Foo. Bien sûr, dans certains cas, ce n'est pas possible. En ce qui concerne couvrir votre code avec shared_ptr<Bar>, la solution la plus simple est de typedef shared_ptr<Bar> à BarPtr ou similaire.

+0

Je pense avoir une bonne raison de passer par ref car il contient plusieurs centaines de données MB. – Frank

+1

Vous pourriez avoir utilisé un shared_ptr interne à ses données. Ensuite, la barre d'adaptation ne copie réellement que le pointeur partagé. – KeithB

0

Vous pouvez simplement déplacer votre objet barre à une portée plus élevée afin d'empêcher sa suppression trop tôt.

int baz() { 
    Bar bar; 
    IFoo* foo = NULL; 
    if(whatever) { 
    foo = new Foo(bar); 
    } 
    else { 
    // create other foo's 
    } 
    foo->test(); // no more segmentation fault 
} 
+0

Merci, mais comme je l'ai écrit dans la question, je ne veux pas vraiment mentionner 'Bar' à l'extérieur du bloc' if (whatever) {} ', car il est vraiment spécifique à ce bloc de code. – Frank

+0

@dehmann: Non, ce n'est pas spécifique à ce bloc de code. Si c'était le cas, vous n'auriez pas le problème. Soit vous le référencez quand vous ne devriez pas, soit vous utilisez une portée trop petite pour cela. –

+0

Il est peut-être temps de casser une Règle et d'introduire un objet global "defaultBar" (ou un singleton) qui peut être référencé lorsque vous devez créer un Foo avec une barre. –

1

Je comprends que vous fournissez une version simplifiée de votre code, et sans voir le reste, je ne sais pas si ce que je dis est maintenant possible.

Depuis foo est l'objet le plus long vivant, comment sur la création de l'instance Bar à l'intérieur, et de faire d'autres références variables (bar dans votre code) à ce sujet? D'autre part, si Bar est vraiment spécifique à l'intérieur du bloc if, test() ne devrait pas du tout en avoir besoin. Dans ce cas, il n'y a aucun problème à garder un pointeur à l'intérieur de Foo puis de le supprimer à la fin du bloc if. Sinon, ce n'est peut-être pas si spécifique au bloc et vous devez repenser un peu votre conception.

+0

Vous avez un bon point quand vous dites que peut-être 'Bar' devrait être créé et détruit dans' Foo'. D'un autre côté, je voulais garder «Foo» petit avec des responsabilités minimales. Je pense que ma situation est la suivante: 'Foo' est une voiture. 'IFoo' est un véhicule en général. 'Bar' est un bon lecteur de DVD. Donc, la voiture contient un lecteur DVD, ce qui n'est certainement pas quelque chose dont un véhicule devrait se soucier en général, donc c'est spécifique à la voiture, mais ce n'est pas qu'un lecteur DVD ne puisse pas vivre en dehors d'une voiture. – Frank

+0

@dehmann: Cela n'a aucun sens pour moi. 'Foo' devrait avoir les responsabilités minimales nécessaires, mais l'une de ses responsabilités minimales consiste à conserver toutes les données qu'il utilise. S'il doit utiliser une barre, il doit s'assurer qu'il garde la barre. –

1

je fait proposer une autre solution ...

Je ne aime vraiment référence sans compter que beaucoup, non seulement l'encombrement, mais aussi parce qu'il est plus difficile de raisonner lorsque plusieurs objets ont une poignée au même élément .

class IBar 
{ 
public: 
    virtual ~IBar() {} 
    virtual IBar* clone() const; 
}; 

class Foo: public IFoo 
{ 
public: 
    Foo(const IBar& ibar): m_bar(ibar.clone()) {} 
    Foo(const Foo& rhs): m_bar(rhs.m_bar->clone()) {} 
    Foo& operator=(const Foo& rhs) { Foo tmp(rhs); this->swap(tmp); return *this; } 
    virtual ~Foo() { delete m_bar; } 

    void swap(Foo& rhs) { std::swap(m_bar, rhs.m_bar); } 

private: 
    IBar* m_bar; 
}; 

Et maintenant votre code fonctionne, parce que je fait une copie :)

Bien sûr, si la barre était pas polymorphes, il serait alors beaucoup plus facile. Donc, même s'il n'utilise pas de référence, êtes-vous sûr d'en avoir réellement besoin?


Obtenons hors piste maintenant, parce qu'il ya un certain nombre de choses que vous avez tort: ​​

  • foo = new Foo(bar) n'est pas assez, comment allez-vous garantir que la mémoire est supprimée dans le cas où un exception se produit ou introduit quelque peu return dans le fouillis if? Vous devez vous déplacer vers un objet qui gère la mémoire pour vous auto_ptr ou le nouveau unique_ptr à partir de C++ 0x ou Boost. Au lieu d'utiliser le comptage des références, pourquoi le Foo ne prend-il pas en charge le code
  • ? Foo(std::auto_ptr<Bar> bar) fonctionnerait aussi.

Shy de la référence de comptage où ce n'est pas nécessaire. Chaque fois que vous partager quelque chose que vous introduisez réellement une source de bogues difficiles à suivre dans votre code (et potentiellement des effets secondaires). Voilà pourquoi Haskell et la famille fonctionnelle gagnent en popularité :)

+0

Ce qui précède a des sémantiques significativement différentes à l'aide d'un pointeur intelligent. Si, par exemple, l'utilisateur doit modifier l'objet, il modifiera sa copie et non l'original. Il y a aussi un coût de performance en fonction de la taille de l'objet. Enfin, si des objets sont stockés dans des listes séparées, de nouveaux objets sont créés pour chaque liste, ce qui peut entraîner un problème de mémoire. –

+0

Oui, il a une sémantique différente. C'est le point après tout. Trop souvent, j'ai vu des gens utiliser 'shared_ptr' juste parce qu'ils ne voulaient pas essayer de comprendre quel était le cycle de vie de l'objet et où il était nécessaire. Les cas où le propriétaire de la mémoire n'est pas clairement défini sont rares. –

+0

Vous faites un bon point en utilisant shread_ptr pour éviter d'enquêter sur le cycle de vie, cependant, pour certains c'est probablement le plus grand positif! Recherche Donc, vous trouverez une réponse à chaque 2ième question: Vous devez utiliser des pointeurs intelligents. Votre solution n'a pas de "propriétaires" de ressources et j'ai du mal à voir comment cet extrême est meilleur que d'utiliser smart_ptrs. En fait, il est susceptible d'utiliser plus de mémoire et d'être plus lent, comment est-ce mieux? –

1

De plus, si vous utilisez ces pointeurs compté référence, vous devez commencer à se soucier si vous voulez vraiment shared_ptr ou plutôt weak_ptr etc

Il n'y a aucun souci si vous suivez votre conception et vous comprenez ce qui est weak_ptr (indice: ce n'est pas une référence) et à quoi cela sert (indice: ce n'est pas pour "casser des cycles").

Si vous commencez à vous inquiéter, c'est parce que vous n'avez jamais eu de design, ou que vous ne comprenez pas le design shared_ptr.