2015-02-19 23 views
7

Da http://en.cppreference.com/w/cpp/memory/unique_ptr: classeQual è il motivo della differenza nel comportamento di distruzione tra std :: unique_ptr e std :: shared_ptr?

Se T è derivato (sic ) di alcuni di base B, allora std::unique_ptr<T> è implicitamente convertibile in std::unique_ptr<B>. Il deleter predefinito di il std::unique_ptr<B> risultante utilizzerà l'operatore delete per B, che conduce al comportamento non definito a meno che il distruttore di B sia virtuale. noti che std::shared_ptr si comporta in modo diverso: std::shared_ptr<B> sarà utilizzare l'operatore delete per il tipo di T e l'oggetto di proprietà saranno eliminati correttamente anche se il distruttore di B non è virtuale.

Qual è il motivo della differenza di comportamento dopo la distruzione descritta sopra? La mia ipotesi iniziale sarebbe la prestazione?

anche interessante sapere è come un std::shared_ptr<B> è in grado di chiamare il distruttore di un tipo T nel caso in cui il distruttore su B non è virtuale e non può essere chiamato, per quanto posso vedere dal contesto della std::shared_ptr<B>?

+1

Questo potrebbe aiutare: http://stackoverflow.com/questions/6876751/differences-between-unique-ptr-and-shared-ptr –

risposta

8

std::shared_ptr<X> ha già un mucchio di spese generali su un grezzo B*.

A shared_ptr<X> fondamentalmente mantiene 4 cose. Mantiene un puntatore a B, mantiene due conteggi di riferimento (un conteggio di riferimento "duro" e uno "soft" per weak_ptr) e mantiene una funzione di pulizia.

La funzione di pulizia è il motivo per cui shared_ptr<X> si comporta in modo diverso. Quando si crea un shared_ptr<X>, una funzione che chiama il distruttore di quel particolare tipo viene creata e archiviata nella funzione di pulizia gestita dallo shared_ptr<X>.

Quando si modificano i tipi gestiti (B* diventa C*), la funzione di pulizia rimane invariata.

Poiché shared_ptr<X> deve gestire i conteggi di riferimento, il sovraccarico aggiuntivo di tale spazio di archiviazione di pulizia è marginale.

Per un unique_ptr<B>, la classe è quasi economica come un grezzo B*. Mantiene lo stato zero diverso dal suo B* e il suo comportamento (alla distruzione) si riduce a if (b) delete b;. (Sì, che if (b) è ridondante, ma un ottimizzatore può capirlo).

Al fine di sostenere ghisa-to-base e delete-as-derivati, lo stato in più avrebbe dovuto essere memorizzato che ricorda il unique_ptr è davvero a una classe derivata. Questo potrebbe essere sotto forma di un puntatore memorizzato a deleter, come un shared_ptr.

Questo, tuttavia, raddoppierà le dimensioni di un unique_ptr<B> o richiederà di memorizzare i dati nell'heap da qualche parte.

È stato deciso che unique_ptr<B> dovrebbe essere zero-overhead e, pertanto, non supporta cast-to-base mentre sta ancora chiamando il distruttore della base.

Ora è possibile insegnare a unique_ptr<B> semplicemente aggiungendo un tipo deleter e archiviando una funzione di distruzione che conosce il tipo di cosa che sta distruggendo. Quanto sopra ha parlato del deleter predefinito di unique_ptr, che è stateless e banale.

struct deleter { 
    void* state; 
    void(*f)(void*); 
    void operator()(void*)const{if (f) f(state);} 
    deleter(deleter const&)=default; 
    deleter(deleter&&o):deleter(o) { o.state = nullptr; o.f=nullptr; } 
    deleter()=delete; 
    template<class T> 
    deleter(T*t): 
    state(t), 
    f([](void*p){delete static_cast<T*>(p);}) 
    {} 
}; 
template<class T> 
using smart_unique_ptr = std::unique_ptr<T, deleter>; 

template<class T, class...Args> 
smart_unique_ptr<T> make_smart_unique(Args&&... args) { 
    T* t = new T(std::forward<Args>(args)...); 
    return { t, t }; 
} 

live example, dove ho generare un unico-PTR TO derivata, conservarlo in un unico-PTR alla base, e quindi reimpostare base. Il puntatore derivato è cancellato.

(Un semplice void(*)(void*) deleter potrebbe incorrere in problemi per cui il passato in void* differiscano valore tra la base e casi derivati.)

noti che cambiando il puntatore memorizzato in tale unique_ptr senza modificare il deleter risulterà nel comportamento mal consigliato.

+0

Buona risposta, ma 'unique_ptr' non è davvero zero-overhead nel caso generale . Deve anche memorizzare un deleter. Diversamente da 'shared_ptr', tuttavia, il tipo deleter è parte del suo tipo (attraverso un parametro template). Per i delet a dimensione zero, l'overheading zero è possibile grazie all'ottimizzazione di base vuota. – Angew

+1

E il 'if (b) delete b;' era corretto. – Angew

+0

@Angew dimentico sempre se fa il test if o no. Penso di aver visto un'implementazione che fa la cosa sbagliata lì. – Yakk

Problemi correlati