shared_ptr magic :)


89

Il signor Lidström e io abbiamo litigato :)

L'affermazione del signor Lidström è che un costrutto shared_ptr<Base> p(new Derived);non richiede che Base abbia un distruttore virtuale:

Armen Tsirunyan : "Davvero? Shared_ptr ripulirà correttamente? Potresti per favore in questo caso dimostrare come questo effetto potrebbe essere implementato?"

Daniel Lidström : " shared_ptr utilizza il proprio distruttore per eliminare l'istanza Concrete. Questo è noto come RAII all'interno della comunità C ++. Il mio consiglio è che impari tutto ciò che puoi su RAII. Renderà il tuo codice C ++ molto più semplice quando lo usi RAII in tutte le situazioni ".

Armen Tsirunyan : "Conosco RAII, e so anche che alla fine il distruttore shared_ptr potrebbe cancellare il px memorizzato quando pn raggiunge 0. Ma se px avesse un puntatore di tipo statico Basee un puntatore di tipo dinamico a Derived, allora a meno che non Baseabbia un distruttore virtuale, questo risulterà in un comportamento indefinito. Correggimi se sbaglio. "

Daniel Lidström : " shared_ptr sa che il tipo statico è Concrete. Lo sa da quando l'ho passato al suo costruttore! Sembra un po 'magico, ma posso assicurarti che è di design ed estremamente carino."

Quindi, giudicaci. Come è possibile (se lo è) implementare shared_ptr senza richiedere alle classi polimorfiche di avere un distruttore virtuale? Grazie in anticipo


3
Avresti potuto collegarti al thread originale .
Darin Dimitrov

8
Un'altra cosa interessante è che shared_ptr<void> p(new Derived)distruggerà anche l' Derivedoggetto con il suo distruttore, indipendentemente dal fatto che lo sia virtualo meno.
dalle

7
Modo fantastico per fare una domanda :)
rubenvb

5
Anche se shared_ptr lo consente, è davvero una cattiva idea progettare una classe come base senza un dtor virtuale. I commenti di Daniel sulla RAII sono fuorvianti - non ha nulla a che fare con questo - ma la conversazione citata suona come un semplice errore di comunicazione (e un'assunzione errata di come funziona shared_ptr).

6
Non RAII, ma piuttosto cancella il tipo del distruttore. Devi stare attento, perché shared_ptr<T>( (T*)new U() )dove struct U:Tnon farà la cosa giusta (e questo può essere fatto indirettamente facilmente, come una funzione che prende una T*e viene passata a U*)
Yakk - Adam Nevraumont

Risposte:


74

Sì, è possibile implementare shared_ptr in questo modo. Boost lo fa e anche lo standard C ++ 11 richiede questo comportamento. Come ulteriore flessibilità shared_ptr gestisce più di un semplice contatore di riferimento. Un cosiddetto deleter viene solitamente inserito nello stesso blocco di memoria che contiene anche i contatori di riferimento. Ma la parte divertente è che il tipo di questo deleter non fa parte del tipo shared_ptr. Questa è chiamata "cancellazione del tipo" ed è fondamentalmente la stessa tecnica usata per implementare le "funzioni polimorfiche" boost :: function o std :: function per nascondere il tipo effettivo del funtore. Per far funzionare il tuo esempio, abbiamo bisogno di un costruttore basato su modelli:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Quindi, se lo usi con le tue classi Base e Derivato ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

... il costruttore basato su modelli con Y = Derived viene utilizzato per costruire l'oggetto shared_ptr. Il costruttore ha quindi la possibilità di creare l'oggetto deleter appropriato e contatori di riferimento e memorizza un puntatore a questo blocco di controllo come membro di dati. Se il contatore dei riferimenti raggiunge lo zero, per eliminare l'oggetto verrà utilizzato il deleter precedentemente creato e in grado di riconoscere Derivati.

Lo standard C ++ 11 ha quanto segue da dire su questo costruttore (20.7.2.2.1):

Richiede: p deve essere convertibile in T*. Ydeve essere un tipo completo. L'espressione delete pdeve essere ben formata, deve avere un comportamento ben definito e non deve generare eccezioni.

Effetti: costruisce un shared_ptroggetto che possiede il puntatore p.

...

E per il distruttore (20.7.2.2.2):

Effetti: se *thisè vuoto o condivide la proprietà con un'altra shared_ptristanza ( use_count() > 1), non ci sono effetti collaterali. Altrimenti, se *thispossiede un oggetto pe un deleter d, d(p)viene chiamato. Altrimenti, if *thispossiede un puntatore pe delete pviene chiamato.

(l'enfasi sull'uso del carattere in grassetto è mia).


the upcoming standard also requires this behaviour: (a) Quale standard e (b) potete fornire un riferimento (allo standard)?
kevinarpe

Voglio solo aggiungere un commento alla risposta di @sellibitze poiché non ho abbastanza punti per add a comment. IMO, è più Boost does thisdi the Standard requires. Non credo che lo Standard lo richieda da quello che sto capendo. Parlando ad esempio 's @sellibitze shared_ptr<Base> sp (new Derived);, richiede di constructorbasta chiedere per delete Derivedessere ben definito e ben formati. Per la specifica di destructor, c'è anche un p, ma non credo che si riferisca a pnella specifica di constructor.
Lujun Weng

28

Quando shared_ptr viene creato, memorizza un oggetto deleter al suo interno. Questo oggetto viene chiamato quando shared_ptr sta per liberare la risorsa puntata. Dato che sai come distruggere la risorsa nel punto di costruzione, puoi usare shared_ptr con tipi incompleti. Chiunque abbia creato shared_ptr vi ha memorizzato un cancellatore corretto.

Ad esempio, puoi creare un deleter personalizzato:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p chiamerà DeleteDerived per distruggere l'oggetto appuntito. L'implementazione lo fa automaticamente.


4
+1 per l'osservazione sui tipi incompleti, molto utile quando si utilizza a shared_ptrcome attributo.
Matthieu M.

16

Semplicemente,

shared_ptr usa una speciale funzione deleter che viene creata dal costruttore che usa sempre il distruttore dell'oggetto dato e non il distruttore di Base, questo è un po 'di lavoro con la meta programmazione dei template, ma funziona.

Qualcosa del genere

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}

1
hmm ... interessante, comincio a crederci :)
Armen Tsirunyan

1
@Armen Tsirunyan Avresti dovuto dare un'occhiata alla descrizione del design di shared_ptr prima di iniziare la discussione. Questa "cattura del deleter" è una delle caratteristiche essenziali di shared_ptr ...
Paul Michalik

6
@ paul_71: sono d'accordo con te. D'altra parte credo che questa discussione sia stata utile non solo per me, ma anche per altre persone che non sapevano questo fatto su shared_ptr. Quindi immagino che non sia stato un grande peccato iniziare comunque questo thread :)
Armen Tsirunyan

3
@Armen Certo che no. Piuttosto, hai fatto un buon lavoro indicando questa caratteristica davvero molto importante di shared_ptr <T> che è spesso supervisionata anche da sviluppatori c ++ esperti.
Paul Michalik
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.