web-dev-qa-db-fra.com

Pourquoi shared_ptr <void> est-il légal, alors que unique_ptr <void> est mal formé?

La question se situe vraiment dans le titre: je suis curieux de savoir quelle est la raison technique de cette différence, mais aussi sa justification?

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
89
Ad N

C'est parce que std::shared_ptr Implémente l'effacement de type, alors que std::unique_ptr Ne le fait pas.


Puisque std::shared_ptr Implémente la suppression de type, il supporte également une autre propriété intéressante , à savoir. il n'a pas besoin du type de deleter comme argument de type de modèle dans le modèle de classe. Regardez leurs déclarations:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

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

template<class T> 
class shared_ptr;

ne l'a pas.

Maintenant, la question est de savoir pourquoi shared_ptr Implémente l'effacement de type? Eh bien, c’est le cas, car il doit prendre en charge le comptage de références, et pour cela, il doit allouer de la mémoire à partir de tas et comme il doit allouer de la mémoire de toute façon, il faut aller un pas plus loin et implémente la suppression de type - qui a aussi besoin d’une allocation de tas. Donc, fondamentalement, il s’agit simplement d’être opportuniste!

En raison de la suppression de type, std::shared_ptr Est capable de supporter deux choses:

  • Il peut stocker des objets de tout type comme void*, , mais il est toujours capable de supprimer les objets lors de la destruction correctement en utilisant correctement en invoquant 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 à peu près tout le fonctionnement de std::shared_ptr.

Maintenant, la question est de savoir si std::unique_ptr Peut stocker des objets as void*? Eh bien, la réponse est yes - à condition que vous passiez un deleter approprié comme argument. Voici une telle démonstration:

int main()
{
    auto deleter = [](void const * data ) {
        int const * p = static_cast<int const*>(data);
        std::cout << *p << " located at " << p <<  " is being deleted";
        delete p;
    };

    std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);

} //p will be deleted here, both p ;-)

Sortie ( démo en ligne ):

959 located at 0x18aec20 is being deleted

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

Dans mon cas, il me faudra un suppresseur de suppression de type, mais cela semble également possible (au prix de l’allocation de tas). Fondamentalement, cela signifie-t-il qu’il existe réellement un emplacement de niche pour un troisième type de pointeur intelligent: un pointeur intelligent de propriété exclusive avec type effacement.

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

Je n’ai jamais réellement essayé cela, mais vous pourriez peut-être y parvenir en utilisant un std::function Approprié comme type de suppression avec unique_ptr? Supposons que cela fonctionne alors que vous avez terminé, propriété exclusive et suppression de type effacée.

Suite à cette suggestion, j’ai implémenté ceci (bien qu’il n’utilise pas std::function Car cela ne semble pas nécessaire):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, [](void const * data) {
         T const * p = static_cast<T const*>(data);
         std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
         delete p;
    });
}

int main()
{
    auto p1 = unique_void(new int(959));
    auto p2 = unique_void(new double(595.5));
    auto p3 = unique_void(new std::string("Hello World"));
}  

Sortie ( démo 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 ça t'as aidé.

104
Nawaz

L'une des raisons est dans l'un des nombreux cas d'utilisation d'un shared_ptr - à savoir comme indicateur de durée de vie ou sentinelle.

Cela a été mentionné dans la documentation d'origine de boost:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(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();
    }
}

closure_target est quelque chose comme ceci:

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

L'appelant enregistre un rappel de la manière suivante:

struct active_object : std::enable_shared_from_this<active_object>
{
    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 
    }
};

car shared_ptr<X> est toujours convertible en shared_ptr<void>, event_emitter peut maintenant ignorer le type d'objet dans lequel il est appelé.

Cette disposition libère les abonnés de l’événement émetteur de l’obligation de gérer les cas de croisement (que se passe-t-il si le rappel est dans une file d’attente, en attente d’être traitée alors que active_object s’éloigne?), Et signifie également qu’il n’est pas nécessaire de synchroniser la désinscription. weak_ptr<void>::lock est une opération synchronisée.

7
Richard Hodges