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

100

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

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
Annonce N
la source

Réponses:

118

C'est parce que std::shared_ptrmet en œuvre l'effacement de type, alors que ce std::unique_ptrn'est pas le cas.


Depuis std::shared_ptrimplémente l'effacement de type, il prend également en charge une autre propriété intéressante, à savoir. il n'a pas besoin du type du suppresseur comme argument de type de modèle pour le modèle de classe. Regardez leurs déclarations:

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

qui a Deletercomme paramètre de type, tandis que

template<class T> 
class shared_ptr;

ne l'a pas.

Maintenant, la question est, pourquoi shared_ptrimplémente l'effacement de type? Eh bien, il le fait, car il doit prendre en charge le comptage de références, et pour prendre en charge cela, il doit allouer de la mémoire à partir du tas et comme il doit allouer de la mémoire de toute façon, il va plus loin et implémente l'effacement de type - qui a besoin de tas allocation aussi. Donc, fondamentalement, il s'agit simplement d'être opportuniste!

En raison de l'effacement de type, std::shared_ptrest capable de prendre en charge deux choses:

  • Il peut stocker des objets de n'importe quel type void*, mais il est toujours capable de supprimer correctement les objets lors de la destruction en invoquant correctement leur destructeur .
  • Le type de deleter n'est pas passé comme argument de type au modèle de classe, ce qui signifie un peu de liberté sans compromettre la sécurité de type .

Bien. Tout dépend de la façon dont cela std::shared_ptrfonctionne.

Maintenant, la question est, peut std::unique_ptrstocker des objets comme void* ? Eh bien, la réponse est oui - à condition que vous passiez un suppresseur 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, j'aurai besoin d'un suppresseur d'effacement de type, mais cela semble également possible (au prix d'une allocation de tas). Fondamentalement, cela signifie-t-il qu'il existe en fait une niche pour un troisième type de pointeur intelligent: un pointeur intelligent de propriété exclusive avec effacement de type.

auquel @Steve Jessop a suggéré la solution suivante,

Je n'ai jamais vraiment essayé cela, mais vous pourriez peut-être y parvenir en utilisant un type approprié std::functioncomme suppression avec unique_ptr? En supposant que cela fonctionne réellement, vous avez terminé, la propriété exclusive et un suppresseur de type effacé.

Suite à cette suggestion, j'ai implémenté ceci (bien que cela ne l'utilise pas std::functioncar 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 cela pourra aider.

Nawaz
la source
13
Bonne réponse, +1. Mais vous pourriez le rendre encore meilleur en mentionnant explicitement qu'un std::unique_ptr<void, D>est toujours possible en fournissant un fichier approprié D.
Angew n'est plus fier de SO
1
@Angrew: Gentil, vous avez trouvé la vraie question sous-jacente qui n'était pas écrite dans ma question;)
Annonce N
@Nawaz: Merci. Dans mon cas, j'aurai besoin d'un suppresseur d'effacement de type, mais cela semble également possible (au prix d'une allocation de tas). Fondamentalement, cela signifie-t-il qu'il existe en fait une niche pour un troisième type de pointeur intelligent: un pointeur intelligent de propriété exclusive avec effacement de type?
Annonce N du
8
@AdN: Je n'ai jamais vraiment essayé cela, mais peut-être pourriez-vous y parvenir en utilisant un std::functiontype de suppression approprié avec unique_ptr? En supposant que cela fonctionne réellement, vous avez terminé, la propriété exclusive et un suppresseur de type effacé.
Steve Jessop le
Grammaire nit: "pourquoi X verbes Y?" devrait être « pourquoi ne X verbe Y? » en anglais.
zwol le
7

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

Cela a été mentionné dans la documentation originale du 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_targetest quelque chose comme ça:

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

L'appelant enregistrerait un rappel quelque chose comme ceci:

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>, le event_emitter peut maintenant être parfaitement inconscient du type d'objet dans lequel il rappelle.

Cette disposition libère les abonnés de l'émetteur d'événements 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 actionné pendant que active_object disparaît?), Et signifie également qu'il n'est pas nécessaire de synchroniser la désinscription. weak_ptr<void>::lockest une opération synchronisée.

Richard Hodges
la source