Qual è l'utilità di `enable_shared_from_this`?


349

Mi sono imbattuto enable_shared_from_thisdurante la lettura degli esempi di Boost.Asio e dopo aver letto la documentazione sono ancora perso per come dovrebbe essere usato correttamente. Qualcuno può darmi un esempio e una spiegazione di quando si utilizza questa classe ha senso.

Risposte:


362

Ti consente di ottenere shared_ptrun'istanza valida per this, quando tutto ciò che hai è this. Senza di essa, si avrebbe alcun modo di ottenere un shared_ptra this, a meno che non hai già avuto uno come membro. Questo esempio dalla documentazione boost per enable_shared_from_this :

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

Il metodo f()restituisce un valido shared_ptr, anche se non ha avuto un'istanza membro. Nota che non puoi semplicemente farlo:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

Il puntatore condiviso che questo restituito avrà un conteggio dei riferimenti diverso da quello "corretto" e uno di essi finirà per perdere e mantenere un riferimento penzolante quando l'oggetto viene eliminato.

enable_shared_from_thisè diventato parte dello standard C ++ 11. Puoi anche ottenerlo da lì e anche da boost.


202
+1. Il punto chiave è che la tecnica "ovvia" di restituire shared_ptr <Y> (questo) è rotta, perché finisce per creare più oggetti shared_ptr distinti con conteggi di riferimento separati. Per questo motivo non devi mai creare più di un shared_ptr dallo stesso puntatore non elaborato .
j_random_hacker,

3
Va notato che in C ++ 11 e versioni successive , è perfettamente valido utilizzare un std::shared_ptrcostruttore su un puntatore non elaborato se eredita da std::enable_shared_from_this. Non so se la semantica di Boost sia stata aggiornata per supportare questo.
Matteo,

6
@MatthewHolder Hai un preventivo per questo? Su cppreference.com ho letto "Costruire un std::shared_ptrper un oggetto che è già gestito da un altro std::shared_ptrnon consulti il ​​riferimento debole memorizzato internamente e quindi porterà a un comportamento indefinito". ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer

5
Perché non puoi farlo shared_ptr<Y> q = p?
Dan M.,

2
@ ThorbjørnLindeijer, hai ragione, è C ++ 17 e versioni successive. Alcune implementazioni hanno seguito la semantica C ++ 16 prima che fosse rilasciata. La gestione corretta per C ++ da 11 a C ++ 14 dovrebbe essere da usare std::make_shared<T>.
Matteo

198

dall'articolo del Dr Dobbs sui puntatori deboli, penso che questo esempio sia più facile da capire (fonte: http://drdobbs.com/cpp/184402026 ):

... codice come questo non funzionerà correttamente:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

Nessuno dei due shared_ptroggetti conosce l'altro, quindi entrambi proveranno a rilasciare la risorsa quando vengono distrutti. Questo di solito porta a problemi.

Allo stesso modo, se una funzione membro ha bisogno di un shared_ptroggetto proprietario dell'oggetto su cui viene chiamato, non può semplicemente creare un oggetto al volo:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

Questo codice presenta lo stesso problema dell'esempio precedente, sebbene in una forma più sottile. Quando viene costruito, l' shared_ptoggetto r sp1possiede la risorsa appena allocata. Il codice all'interno della funzione membro S::dangerousnon conosce shared_ptrquell'oggetto, quindi l' shared_ptroggetto da esso restituito è distinto sp1. Copiare il nuovo shared_ptroggetto sp2non aiuta; quando sp2esce dall'ambito, rilascia la risorsa e quando sp1esce dall'ambito, rilascerà nuovamente la risorsa.

Il modo per evitare questo problema è utilizzare il modello di classe enable_shared_from_this. Il modello accetta un argomento del tipo di modello, che è il nome della classe che definisce la risorsa gestita. A sua volta, tale classe deve essere derivata pubblicamente dal modello; come questo:

struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

Quando lo fai, tieni presente che l'oggetto su cui chiami shared_from_thisdeve essere di proprietà di un shared_ptroggetto. Questo non funzionerà:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}

15
Grazie, questo illustra il problema risolto meglio della risposta attualmente accettata.
goertzenator,

2
+1: buona risposta. A parte questo, invece di shared_ptr<S> sp1(new S);può essere preferito usarlo shared_ptr<S> sp1 = make_shared<S>();, vedi ad esempio stackoverflow.com/questions/18301511/…
Arun

4
Sono abbastanza sicuro che l'ultima riga dovrebbe leggere shared_ptr<S> sp2 = p->not_dangerous();perché la trappola qui è che devi creare un shared_ptr nel modo normale prima di chiamare shared_from_this()la prima volta! È davvero facile sbagliarsi! Prima di C ++ 17 è UB chiamare shared_from_this()prima che esattamente un shared_ptr sia stato creato nel modo normale: auto sptr = std::make_shared<S>();o shared_ptr<S> sptr(new S());. Per fortuna dal C ++ 17 in poi, lo farà.
AnorZaken,


2
@AnorZaken Ottimo punto. Sarebbe stato utile se tu avessi inviato una richiesta di modifica per fare quella correzione. L'ho appena fatto. L'altra cosa utile sarebbe stata per il poster di non scegliere nomi di metodi soggettivi e sensibili al contesto!
underscore_d

30

Ecco la mia spiegazione, dal punto di vista generale (la risposta principale non mi ha "cliccato"). * Si noti che questo è il risultato dell'indagine dell'origine di shared_ptr e enable_shared_from_this fornita con Visual Studio 2012. Forse altri compilatori implementano enable_shared_from_this in modo diverso ... *

enable_shared_from_this<T>aggiunge weak_ptr<T>un'istanza privata a Tcui è associato il " conteggio dei veri riferimenti " per l'istanza di T.

Quindi, quando si crea per la prima volta un shared_ptr<T>su un nuovo T *, il suo debole_ptr interno di T * viene inizializzato con un refcount di 1. Il nuovo shared_ptrfondamentalmente torna su questo weak_ptr.

Tpuò quindi, nei suoi metodi, chiamare shared_from_thisper ottenere un'istanza di shared_ptr<T>tali backup sullo stesso conteggio dei riferimenti memorizzati internamente . In questo modo, hai sempre un posto in cui T*è memorizzato il conteggio dei riferimenti invece di avere più shared_ptristanze che non si conoscono a vicenda, e ognuna pensa di essere il shared_ptrresponsabile del conteggio dei ref Te dell'eliminazione quando il loro riferimento -count raggiunge lo zero.


1
Questo è corretto, e la parte veramente importante è So, when you first create...perché è un requisito (come dici tu, il punto debole non è inizializzato fino a quando non passi il puntatore degli oggetti in un ctor shared_ptr!) E questo requisito è dove le cose possono andare terribilmente sbagliate se lo sei non attento. Se non crei shared_ptr prima di chiamare shared_from_thisottieni UB - allo stesso modo se crei più di shared_ptr ottieni anche UB. Devi in ​​qualche modo assicurarti di creare un shared_ptr esattamente una volta.
AnorZaken,

2
In altre parole, l'intera idea di enable_shared_from_thisè fragile per cominciare dal momento che il punto è quello di essere in grado di ottenere un shared_ptr<T>da a T*, ma in realtà quando si ottiene un puntatore T* tin genere non è sicuro supporre che qualcosa sia già condiviso o no, e fare l'ipotesi sbagliata è UB.
AnorZaken,

" internal weak_ptr viene inizializzato con un refcount di 1 " ptr debole a T non possiede un ptr intelligente a T. Un debole ptr è un ref intelligente proprietario di informazioni sufficienti per creare un ptr proprietario che è una "copia" di un altro ptr proprietario. Un ptr debole non ha conteggi di riferimento. Ha accesso a un conteggio di riferimento, come tutti i proprietari di rif.
curioso

3

Si noti che l'utilizzo di un boost :: intrusive_ptr non presenta questo problema. Questo è spesso un modo più conveniente per aggirare questo problema.


Sì, ma enable_shared_from_thisti consente di lavorare con un'API che accetta specificamente shared_ptr<>. A mio avviso, un'API di questo genere sta facendo Do It It Wrong (poiché è meglio lasciare che qualcosa di più alto nello stack possieda la memoria) ma se sei costretto a lavorare con tale API, questa è una buona opzione.
cdunn2001,

2
Meglio rimanere nello standard il più possibile.
Sergei,

3

È esattamente lo stesso in c ++ 11 e versioni successive: è per abilitare la possibilità di tornare thiscome puntatore condiviso poiché thisti dà un puntatore non elaborato.

in altre parole, ti permette di trasformare il codice in questo modo

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

in questo:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           

Funzionerà solo se questi oggetti sono sempre gestiti da a shared_ptr. Potresti voler cambiare l'interfaccia per assicurarti che sia il caso.
curioso

1
Hai assolutamente ragione @curiousguy. Questo è ovvio. Mi piace anche scrivere tutto il mio shared_ptr per migliorare la leggibilità durante la definizione delle mie API pubbliche. Ad esempio, invece di std::shared_ptr<Node> getParent const(), normalmente lo espongo come NodePtr getParent const()invece. Se hai assolutamente bisogno dell'accesso al puntatore non elaborato interno (esempio migliore: occuparsi di una libreria C), ce n'è std::shared_ptr<T>::getper quello, che odio menzionare perché ho questo acceditore puntatore non elaborato usato troppe volte per la ragione sbagliata.
mchiasson,

-3

Un altro modo è quello di aggiungere un weak_ptr<Y> m_stubmembro in class Y. Allora scrivi:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Utile quando non è possibile modificare la classe da cui si deriva (ad es. Estendere la biblioteca di altre persone). Non dimenticare di inizializzare il membro, ad esempio m_stub = shared_ptr<Y>(this), è valido anche durante un costruttore.

Va bene se ci sono più tronchi come questo nella gerarchia ereditaria, non impedirà la distruzione dell'oggetto.

Modifica: Come sottolineato correttamente dall'utente nobar, il codice distruggerebbe l'oggetto Y al termine dell'assegnazione e le variabili temporanee verranno distrutte. Pertanto la mia risposta non è corretta.


4
Se la tua intenzione qui è quella di produrre un shared_ptr<>che non elimini la sua punta, questo è eccessivo. Puoi semplicemente dire return shared_ptr<Y>(this, no_op_deleter);dov'è che no_op_deleterun oggetto funzione unario prende Y*e non fa nulla.
John Zwinck,

2
Sembra improbabile che questa sia una soluzione funzionante. m_stub = shared_ptr<Y>(this)costruirà e distruggerà immediatamente un shared_ptr temporaneo da questo. Al termine di questa affermazione, thisverranno eliminati e tutti i riferimenti successivi saranno sospesi.
nobar,

2
L'autore riconosce che questa risposta è sbagliata, quindi probabilmente potrebbe semplicemente cancellarla. Ma ha effettuato l'ultimo accesso in 4,5 anni, quindi non è probabile che lo faccia - qualcuno con poteri più alti potrebbe rimuovere questa aringa rossa?
Tom Goodfellow,

se si osserva l'implementazione di enable_shared_from_this, mantiene un valore weak_ptrdi se stesso (popolato dal ctor), restituito come shared_ptrquando si chiama shared_from_this. In altre parole, stai duplicando ciò che enable_shared_from_thisgià fornisce.
mchiasson,
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.