shared_ptr magic :)

89

M. Lidström et moi nous sommes disputés :)

L'affirmation de M. Lidström est qu'une construction shared_ptr<Base> p(new Derived);ne nécessite pas que Base ait un destructeur virtuel:

Armen Tsirunyan : "Vraiment? Le shared_ptr nettoiera-t-il correctement? Pourriez-vous s'il vous plaît dans ce cas démontrer comment cet effet pourrait être mis en œuvre?"

Daniel Lidström : "Le shared_ptr utilise son propre destructeur pour supprimer l'instance Concrete. Ceci est connu sous le nom de RAII dans la communauté C ++. Mon conseil est que vous appreniez tout ce que vous pouvez sur RAII. Cela rendra votre codage C ++ tellement plus facile lorsque vous l'utiliserez RAII dans toutes les situations. "

Armen Tsirunyan : "Je connais RAII, et je sais aussi que finalement le destructeur shared_ptr peut supprimer le px stocké lorsque pn atteint 0. Mais si px avait un pointeur de type statique vers Baseet un pointeur de type dynamique vers Derived, alors à moins d' Baseavoir un destructeur virtuel, cela entraînera un comportement indéfini. Corrigez-moi si je me trompe. "

Daniel Lidström : "Le shared_ptr sait que le type statique est Concrete. Il le sait depuis que je l'ai passé dans son constructeur! Cela semble un peu magique, mais je peux vous assurer que c'est par conception et extrêmement agréable."

Alors, jugez-nous. Comment est-il possible (si c'est le cas) d'implémenter shared_ptr sans exiger que les classes polymorphes aient un destructeur virtuel? Merci d'avance

Armen Tsirunyan
la source
3
Vous auriez pu créer un lien vers le fil d'origine .
Darin Dimitrov
8
Une autre chose intéressante est que shared_ptr<void> p(new Derived)cela détruira également l' Derivedobjet par son destructeur, qu'il le soit virtualou non.
dalle
7
Super moyen de poser une question :)
rubenvb
5
Même si shared_ptr le permet, c'est une très mauvaise idée de concevoir une classe comme une base sans dtor virtuel. Les commentaires de Daniel sur RAII sont trompeurs - cela n'a rien à voir avec cela - mais la conversation citée ressemble à une simple erreur de communication (et à une hypothèse incorrecte sur le fonctionnement de shared_ptr).
6
Pas RAII, mais plutôt efface le destructeur. Vous devez être prudent, car shared_ptr<T>( (T*)new U() )struct U:Tne fera pas la bonne chose (et cela peut être fait indirectement facilement, comme une fonction qui prend un T*et est passé a U*)
Yakk - Adam Nevraumont

Réponses:

74

Oui, il est possible d'implémenter shared_ptr de cette façon. Boost le fait et la norme C ++ 11 requiert également ce comportement. De plus, shared_ptr gère plus qu'un simple compteur de référence. Un soi-disant suppresseur est généralement placé dans le même bloc de mémoire qui contient également les compteurs de référence. Mais la partie amusante est que le type de ce suppresseur ne fait pas partie du type shared_ptr. C'est ce qu'on appelle "l'effacement de type" et c'est fondamentalement la même technique utilisée pour implémenter les "fonctions polymorphes" boost :: function ou std :: function pour cacher le type du foncteur réel. Pour que votre exemple fonctionne, nous avons besoin d'un constructeur basé sur un modèle:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Donc, si vous utilisez ceci avec vos classes Base et Derived ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

... le constructeur basé sur un modèle avec Y = Derived est utilisé pour construire l'objet shared_ptr. Le constructeur a ainsi la possibilité de créer l'objet de suppression et les compteurs de référence appropriés et stocke un pointeur vers ce bloc de contrôle en tant que membre de données. Si le compteur de référence atteint zéro, le suppresseur créé précédemment et prenant en charge les dérivés sera utilisé pour éliminer l'objet.

La norme C ++ 11 a ce qui suit à propos de ce constructeur (20.7.2.2.1):

Nécessite: p doit être convertible en T*. Ydoit être un type complet. L'expression delete pdoit être bien formée, doit avoir un comportement bien défini et ne doit pas lancer d'exceptions.

Effets: construit un shared_ptrobjet qui possède le pointeur p.

Et pour le destructeur (20.7.2.2.2):

Effets: si *thisest vide ou partage la propriété avec une autre shared_ptrinstance ( use_count() > 1), il n'y a pas d'effets secondaires. Sinon, si *thispossède un objet pet un suppresseur d, d(p)est appelé. Sinon, if *thispossède un pointeur pet delete pest appelé.

(Je souligne l'utilisation de la police en gras).

sellibitze
la source
the upcoming standard also requires this behaviour: (a) Quelle norme et (b) pouvez-vous fournir une référence (à la norme)?
kevinarpe
Je veux juste ajouter un commentaire à la réponse de @sellibitze car je n'ai pas assez de points add a comment. IMO, c'est plus Boost does thisque the Standard requires. Je ne pense pas que la norme l'exige d'après ce que je comprends. En parlant de l » exemple de @sellibitze shared_ptr<Base> sp (new Derived);, nécessite de constructorsimplement demander d' delete Derivedêtre bien défini et bien formé. Pour la spécification de destructor, il existe également un p, mais je ne pense pas que cela fasse référence au pdans la spécification de constructor.
Lujun Weng
28

Lorsque shared_ptr est créé, il stocke un objet de suppression à l' intérieur de lui-même. Cet objet est appelé lorsque shared_ptr est sur le point de libérer la ressource pointée. Puisque vous savez comment détruire la ressource au moment de la construction, vous pouvez utiliser shared_ptr avec des types incomplets. Celui qui a créé le shared_ptr y a stocké un suppresseur correct.

Par exemple, vous pouvez créer un suppresseur personnalisé:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p appellera DeleteDerived pour détruire l'objet pointé. L'implémentation le fait automatiquement.

Yakov Galka
la source
4
+1 pour la remarque sur les types incomplets, très pratique lors de l'utilisation de a shared_ptrcomme attribut.
Matthieu M.
16

Simplement,

shared_ptr utilise une fonction de suppression spéciale créée par un constructeur qui utilise toujours le destructeur de l'objet donné et non le destructeur de Base, c'est un peu de travail avec la méta-programmation de modèle, mais cela fonctionne.

Quelque chose comme ca

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}
Artyom
la source
1
hmm ... intéressant, je commence à y croire :)
Armen Tsirunyan
1
@Armen Tsirunyan Vous devriez avoir jeté un œil dans la description de conception du shared_ptr avant de commencer la discussion. Cette `` capture du déléteur '' est l'une des fonctionnalités essentielles de shared_ptr ...
Paul Michalik
6
@ paul_71: Je suis d'accord avec vous. D'un autre côté, je pense que cette discussion a été utile non seulement pour moi, mais aussi pour d'autres personnes qui ne savaient pas ce fait sur shared_ptr. Donc, je suppose que ce n'était pas un grand péché de commencer ce fil de toute façon :)
Armen Tsirunyan
3
@Armen Bien sûr que non. Au contraire, vous avez fait du bon travail en indiquant cette fonctionnalité vraiment très importante de shared_ptr <T> qui est souvent supervisée même par des développeurs C ++ expérimentés.
Paul Michalik