Pourquoi shared_ptr legal, alors que unique_ptr est mal formé?

La question tient vraiment dans le titre: je suis curieux de savoir quelle est la raison technique de cette différence, mais aussi la raison d’être?

std::shared_ptr sharedToVoid; // legal; std::unique_ptr uniqueToVoid; // ill-formed; 

C’est parce que std::shared_ptr implémente le type-erasure, contrairement à std::unique_ptr .


Comme std::shared_ptr implémente le type-erasure, il supporte également une autre propriété intéressante, à savoir: il n’a pas besoin du type du paramètre deleter en tant qu’argument de type modèle pour le modèle de classe. Regardez leurs déclarations:

 template > class unique_ptr; 

qui a Deleter comme paramètre de type, alors que

 template class shared_ptr; 

ne l’a pas

Maintenant, la question est la suivante: pourquoi shared_ptr implémente-t-il l’effacement de type? Eh bien, il le fait, car il doit prendre en charge le comptage des références, et pour cela, il doit allouer de la mémoire depuis le tas et, de toute façon, il doit allouer de la mémoire, ce qui nécessite un tas allocation aussi. Donc, fondamentalement, c’est juste être opportuniste!

À cause de type-erasure, std::shared_ptr peut supporter deux choses:

  • Il peut stocker des objects de tout type comme void* , mais il est toujours capable de supprimer les objects lors de la destruction correctement en appelant correctement leur destructeur .
  • Le type de deleter n’est pas transmis en tant qu’argument de type au modèle de classe, ce qui signifie un peu de liberté sans compromettre la sécurité de type .

Bien. C’est tout ce qui concerne le fonctionnement de std::shared_ptr .

Maintenant, la question est la suivante: std::unique_ptr stocker des objects comme void* ? Eh bien, la réponse est oui – à condition que vous transmettiez un argument approprié comme argument. Voici une démonstration de ce type:

 int main() { auto deleter = [](void const * data ) { int const * p = static_cast(data); std::cout << *p << " located at " << p << " is being deleted"; delete p; }; std::unique_ptr p(new int(959), deleter); } //p will be deleted here, both p ;-) 

Sortie ( démonstration en ligne ):

 959 located at 0x18aec20 is being deleted 

Vous avez posé une question très intéressante dans le commentaire:

Dans mon cas, j’aurai besoin d’un deleter d’effacement de type, mais cela semble également possible (au prix d’une allocation de tas). Fondamentalement, cela signifie-t-il qu’il existe un créneau spécifique pour un troisième type de pointeur intelligent: un pointeur intelligent de propriété exclusive avec un effacement de type.

à laquelle @Steve Jessop a suggéré la solution suivante,

Je n’ai jamais vraiment essayé ceci, mais peut-être pourriez-vous y parvenir en utilisant une fonction std::function appropriée comme type deleter avec unique_ptr ? En supposant que cela fonctionne réellement, vous avez terminé, propriété exclusive et un deleter effacé.

Suite à cette suggestion, j’ai implémenté ceci,

 using deleter_t = std::function; using unique_void_ptr = std::unique_ptr; template auto deleter(void const * data) -> void { T const * p = static_cast(data); std::cout << "{" << *p << "} located at [" << p << "] is being deleted.\n"; delete p; } template auto unique_void(T * ptr) -> unique_void_ptr { return unique_void_ptr(ptr, &deleter); } int main() { auto p1 = unique_void(new int(959)); auto p2 = unique_void(new double(595.5)); auto p3 = unique_void(new std::ssortingng("Hello World")); } 

Sortie ( démonstration en ligne ):

 {Hello World} located at [0x2364c60] is being deleted. {595.5} located at [0x2364c40] is being deleted. {959} located at [0x2364c20] is being deleted. 

J’espère que cela pourra aider.

L’une des raisons réside dans l’un des nombreux cas d’utilisation d’un shared_ptr – à savoir en tant qu’indicateur de durée de vie ou sentinelle.

Cela a été mentionné dans la documentation de boost originale:

 auto register_callback(std::function closure, std::shared_ptr pv) { auto closure_target = { closure, std::weak_ptr(pv) }; ... // store the target somewhere, and later.... } void call_closure(closure_target target) { // test whether target of the closure still exists auto lock = target.sentinel.lock(); if (lock) { // if so, call the closure target.closure(); } } 

Où système de closure_target est quelque chose comme ceci:

 struct closure_target { std::function closure; std::weak_ptr sentinel; }; 

L’appelant enregistre un rappel comme ceci:

 struct active_object : std::enable_shared_from_this { void start() { event_emitter_.register_callback([this] { this->on_callback(); }, shared_from_this()); } void on_callback() { // this is only ever called if we still exist } }; 

Parce que shared_ptr est toujours convertible en shared_ptr , event_emitter peut maintenant ignorer le type d’object dans lequel il est rappelé.

Cette disposition libère les abonnés de l’émetteur d’événement de l’obligation de traiter les cas croisés (et si le rappel sur une queue, en attente d’action, pendant que l’object actif disparaît?), Et aussi de ne pas synchroniser la désinscription. weak_ptr::lock est une opération synchronisée.