Perché shared_ptr <void> è legale, mentre unique_ptr <void> è mal formato?


100

La domanda rientra davvero nel titolo: sono curioso di sapere qual è la ragione tecnica di questa differenza, ma anche la logica?

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

Risposte:


118

È perché std::shared_ptrimplementa la cancellazione del tipo, mentre std::unique_ptrnon lo fa.


Poiché std::shared_ptrimplementa la cancellazione del tipo, supporta anche un'altra proprietà interessante, vale a dire. non ha bisogno del tipo di deleter come argomento del tipo di modello per il modello di classe. Guarda le loro dichiarazioni:

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

che ha Deletercome parametro di tipo, while

template<class T> 
class shared_ptr;

non ce l'ha.

La domanda è: perché shared_ptrimplementa la cancellazione del tipo? Bene, lo fa, perché deve supportare il conteggio dei riferimenti e, per supportarlo, deve allocare la memoria dall'heap e poiché deve allocare comunque la memoria, fa un ulteriore passo avanti e implementa la cancellazione del tipo, che ha bisogno dell'heap anche l'allocazione. Quindi fondamentalmente si tratta solo di essere opportunisti!

A causa della cancellazione del tipo, std::shared_ptrè in grado di supportare due cose:

  • Può memorizzare oggetti di qualsiasi tipo void*, ma è ancora in grado di eliminare correttamente gli oggetti durante la distruzione invocando correttamente il loro distruttore .
  • Il tipo di deleter non viene passato come argomento di tipo al modello di classe, il che significa un po 'di libertà senza compromettere l'indipendenza dai tipi .

Tutto a posto. Questo è tutto su come std::shared_ptrfunziona.

La domanda è: può std::unique_ptrmemorizzare oggetti come void* ? Ebbene, la risposta è , a patto di passare un appropriato deleter come argomento. Ecco una di queste dimostrazioni:

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 ;-)

Uscita ( demo online ):

959 located at 0x18aec20 is being deleted

Hai posto una domanda molto interessante nel commento:

Nel mio caso avrò bisogno di un deleter per la cancellazione dei tipi, ma sembra anche possibile (a costo di un'allocazione di heap). Fondamentalmente, questo significa che esiste in realtà un posto di nicchia per un terzo tipo di puntatore intelligente: un puntatore intelligente di proprietà esclusiva con cancellazione del tipo.

a cui @Steve Jessop ha suggerito la seguente soluzione,

In realtà non l'ho mai provato, ma forse potresti ottenerlo usando un appropriato std::functioncome tipo di deleter con unique_ptr? Supponendo che funzioni effettivamente, hai finito, proprietà esclusiva e un deleter cancellato dal tipo.

Seguendo questo suggerimento, l'ho implementato (anche se non fa uso std::functionin quanto non sembra necessario):

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"));
}  

Uscita ( demo online ):

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

Spero che aiuti.


13
Buona risposta, +1. Ma potresti renderlo ancora migliore menzionando esplicitamente che a std::unique_ptr<void, D>è ancora possibile fornendo un adatto D.
Angew non è più orgoglioso di SO

1
@Angrew: Bella, hai trovato la vera domanda di fondo che non era scritta nella mia domanda;)
Annuncio N

@Nawaz: grazie. Nel mio caso avrò bisogno di un deleter per la cancellazione dei tipi, ma sembra anche possibile (a costo di un'allocazione di heap). Fondamentalmente, questo significa che esiste effettivamente un posto di nicchia per un terzo tipo di puntatore intelligente: un puntatore intelligente di proprietà esclusiva con cancellazione del tipo?
Annuncio N

8
@AdN: non l'ho mai provato, ma forse potresti ottenerlo usando un tipo appropriato std::functioncome deleter con unique_ptr? Supponendo che funzioni effettivamente, hai finito, proprietà esclusiva e un deleter cancellato dal tipo.
Steve Jessop

Grammatica nit: "perché X verbi Y?" dovrebbe essere "perché fa X verbo Y?" in inglese.
Zwol

7

Una delle ragioni è in uno dei tanti casi d'uso di a shared_ptr- vale a dire come indicatore di vita o sentinella.

Questo è stato menzionato nella documentazione originale del 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();
    }
}

Dov'è closure_targetqualcosa del genere:

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

Il chiamante registrerebbe una richiamata simile a questa:

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 
    }
};

poiché shared_ptr<X>è sempre convertibile in shared_ptr<void>, event_emitter può ora essere beatamente inconsapevole del tipo di oggetto in cui sta richiamando.

Questa disposizione rilascia gli abbonati all'emittente dell'evento dall'obbligo di gestire i casi di attraversamento (cosa succede se la richiamata è in coda, in attesa di essere eseguita mentre active_object scompare?), E significa anche che non è necessario sincronizzare l'annullamento. weak_ptr<void>::lockè un'operazione sincronizzata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.