Quali sono i potenziali pericoli quando si utilizza boost :: shared_ptr?

Quali sono alcuni modi in cui puoi boost::shared_ptr ai piedi quando usi boost::shared_ptr ? In altre parole, quali sono le insidie ​​che devo evitare quando uso boost::shared_ptr ?

Riferimenti ciclici: a shared_ptr<> a qualcosa che ha un shared_ptr<> all’object originale. È ansible utilizzare weak_ptr<> per interrompere questo ciclo, ovviamente.


Aggiungo il seguente esempio di ciò di cui sto parlando nei commenti.

 class node : public enable_shared_from_this { public : void set_parent(shared_ptr parent) { parent_ = parent; } void add_child(shared_ptr child) { children_.push_back(child); child->set_parent(shared_from_this()); } void frob() { do_frob(); if (parent_) parent_->frob(); } private : void do_frob(); shared_ptr parent_; vector< shared_ptr > children_; }; 

In questo esempio, hai un albero di nodes, ognuno dei quali contiene un puntatore al suo genitore. La funzione membro frob (), per qualsiasi motivo, si increspa verso l’alto attraverso l’albero. (Questo non è del tutto stravagante, alcuni framework GUI funzionano in questo modo).

Il problema è che, se perdi il riferimento al nodo più in alto, il nodo più in alto contiene ancora forti riferimenti ai suoi figli, e tutti i suoi figli hanno anche un forte riferimento ai loro genitori. Ciò significa che ci sono riferimenti circolari che mantengono tutte le istanze dal ripulirsi, mentre non c’è modo di raggiungere effettivamente l’albero dal codice, questa perdita di memoria.

 class node : public enable_shared_from_this { public : void set_parent(shared_ptr parent) { parent_ = parent; } void add_child(shared_ptr child) { children_.push_back(child); child->set_parent(shared_from_this()); } void frob() { do_frob(); shared_ptr parent = parent_.lock(); // Note: parent_.lock() if (parent) parent->frob(); } private : void do_frob(); weak_ptr parent_; // Note: now a weak_ptr<> vector< shared_ptr > children_; }; 

Qui, il nodo genitore è stato sostituito da un puntatore debole. Non ha più voce nella vita del nodo a cui si riferisce. Pertanto, se il nodo più in alto esce dall’ambito dell’esempio precedente, mentre contiene forti riferimenti ai suoi figli, i suoi figli non mantengono forti riferimenti ai loro genitori. Quindi non ci sono forti riferimenti all’object e si ripulisce. A sua volta, questo fa sì che i bambini perdano il loro unico riferimento forte, che li fa ripulire, e così via. In breve, questo non perde. E semplicemente sostituendo strategicamente un shared_ptr <> con un weak_ptr <>.

Nota: quanto sopra vale anche per std :: shared_ptr <> e std :: weak_ptr <> come per boost :: shared_ptr <> e boost :: weak_ptr <>.

Creazione di più shared_ptr non correlate allo stesso object:

 #include  #include "boost/shared_ptr.hpp" class foo { public: foo() { printf( "foo()\n"); } ~foo() { printf( "~foo()\n"); } }; typedef boost::shared_ptr pFoo_t; void doSomething( pFoo_t p) { printf( "doing something...\n"); } void doSomethingElse( pFoo_t p) { printf( "doing something else...\n"); } int main() { foo* pFoo = new foo; doSomething( pFoo_t( pFoo)); doSomethingElse( pFoo_t( pFoo)); return 0; } 

Costruire un puntatore condiviso temporaneo anonimo, ad esempio all’interno degli argomenti di una chiamata di funzione:

 f(shared_ptr(new Foo()), g()); 

Questo perché è permesso che il new Foo() sia eseguito, quindi g() chiama, e g() per lanciare un’eccezione, senza che il shared_ptr sia mai stato impostato, quindi il shared_ptr non ha la possibilità di ripulire l’object Foo .

Fai attenzione a fare due puntatori allo stesso object.

 boost::shared_ptr b( new Derived() ); { boost::shared_ptr d( b.get() ); } // d goes out of scope here, deletes pointer b->doSomething(); // crashes 

invece usa questo

 boost::shared_ptr b( new Derived() ); { boost::shared_ptr d = boost::dynamic_pointer_cast( b ); } // d goes out of scope here, refcount-- b->doSomething(); // no crash 

Inoltre, qualsiasi class che detiene shared_ptrs dovrebbe definire i costruttori di copia e gli operatori di assegnazione.

Non provare a usare shared_from_this () nel costruttore – non funzionerà. Invece crea un metodo statico per creare la class e fagli restituire una shared_ptr.

Ho passato i riferimenti a shared_ptrs senza problemi. Assicurati solo che sia copiato prima di essere salvato (cioè, nessun riferimento come membro della class).

Ecco due cose da evitare:

  • Chiamando la funzione get() per ottenere il puntatore raw e utilizzarlo dopo che l’object puntato esce dall’ambito.

  • Passare un riferimento o un puntatore non shared_ptr a un shared_ptr dovrebbe essere pericoloso, poiché non incrementerà il conteggio interno che aiuta a mantenere vivo l’object.

Facciamo il debug di diverse settimane con strani comportamenti.

La ragione era:
abbiamo passato ‘this’ ad alcuni thread worker invece di ‘shared_from_this’.

Non proprio una pistola a pedale, ma sicuramente una fonte di frustrazione fino a che non ti giri in testa come farlo in C ++ 0x: la maggior parte dei predicati che conosci e che ami da non giocano bene con shared_ptr . Fortunatamente, std::tr1::mem_fn funziona con oggetti, puntatori e shared_ptr s, sostituendo std::mem_fun , ma se vuoi usare std::negate , std::not1 , std::plus o qualcuno di quei vecchi amici con shared_ptr , preparatevi a stare bene con std::tr1::bind e probabilmente anche con argomenti placeholder. In pratica questo è in realtà molto più generico, dal momento che in pratica finisci per utilizzare il bind per ogni adattatore di oggetti funzione, ma ci vuole un po ‘di tempo per abituarsi se hai già familiarità con le funzioni di comodità dell’STL.

Questo articolo di DDJ tocca l’argomento, con un sacco di codice di esempio. Ne ho parlato anche alcuni anni fa quando ho dovuto capire come farlo.

Usare shared_ptr per oggetti molto piccoli (come char short ) potrebbe essere un overhead se hai molti piccoli oggetti in heap ma non sono realmente “condivisi”. boost::shared_ptr alloca 16 byte per ogni nuovo conteggio di riferimento creato su g ++ 4.4.3 e VS2008 con Boost 1.42. std::tr1::shared_ptr alloca 20 byte. Ora se hai un milione shared_ptr distinto shared_ptr significa che 20 milioni di byte di memoria sono andati in possesso di count = 1. Per non parlare dei costi indiretti e della frammentazione della memoria. Prova con quanto segue sulla tua piattaforma preferita.

 void * operator new (size_t size) { std::cout << "size = " << size << std::endl; void *ptr = malloc(size); if(!ptr) throw std::bad_alloc(); return ptr; } void operator delete (void *p) { free(p); } 

Dare un shared_ptr a questo all’interno di una definizione di class è anche pericoloso. Utilizza invece enabled_shared_from_this.

Vedi il seguente post qui

Devi stare attento quando usi shared_ptr nel codice multithread. È quindi relativamente facile diventare un caso quando una coppia di shared_ptr , che punta alla stessa memoria, viene utilizzata da thread diversi.

L’uso diffuso e diffuso di shared_ptr causerà quasi inevitabilmente occupazione di memoria indesiderata e non vista.

I riferimenti ciclici sono una causa ben nota e alcuni di essi possono essere indiretti e difficili da individuare, specialmente nel codice complesso su cui lavorano più programmatori; un programmatore può decidere che un object ha bisogno di un riferimento a un altro come una soluzione rapida e non ha il tempo di esaminare tutto il codice per vedere se sta chiudendo un ciclo. Questo rischio è enormemente sottostimato.

Meno ben compreso è il problema dei riferimenti inediti. Se un object è condiviso con molti shared_ptrs, non verrà distrutto fino a quando ognuno di essi non viene azzerato o esce dallo scope. È molto facile trascurare uno di questi riferimenti e ritrovarsi con oggetti in agguato nella memoria che pensavi di aver finito.

Sebbene in senso stretto non si tratti di perdite di memoria (verranno tutte rilasciate prima che il programma venga interrotto), sono altrettanto dannose e difficili da rilevare.

Questi problemi sono le conseguenze di dichiarazioni false opportune: 1. Dichiarare ciò che si vuole veramente essere proprietà singola come shared_ptr. scoped_ptr sarebbe corretto ma qualsiasi altro riferimento a quell’object dovrà essere un puntatore raw, che potrebbe essere lasciato penzolare. 2. Dichiarare ciò che si vuole veramente essere un riferimento osservativo passivo come shared_ptr. weak_ptr sarebbe corretto ma poi hai il problema di convertirlo in share_ptr ogni volta che vuoi usarlo.

Sospetto che il tuo progetto sia un bell’esempio del tipo di problemi a cui questa pratica può farti coinvolgere.

Se hai un’applicazione che richiede memoria intensa, hai davvero bisogno di una singola proprietà in modo che il tuo design possa controllare in modo esplicito le durate degli oggetti.

Con proprietà singola opObject = NULL; eliminerà definitivamente l’object e lo farà ora.

Con proprietà condivisa spObject = NULL; ……..chissà?……

Se si dispone di un registro degli oggetti condivisi (un elenco di tutte le istanze attive, ad esempio), gli oggetti non verranno mai liberati. Soluzione: come nel caso delle strutture di dipendenza circolare (vedere la risposta di Kaz Dragon), utilizzare weak_ptr come appropriato.

I puntatori intelligenti non sono per tutto e i puntatori grezzi non possono essere eliminati

Probabilmente il peggior pericolo è che poiché shared_ptr è uno strumento utile, le persone inizieranno a metterlo ovunque. Poiché i semplici puntatori possono essere utilizzati in modo scorretto, le stesse persone cercano puntatori grezzi e cercano di sostituirli con stringhe, contenitori o puntatori intelligenti anche quando non ha senso. Gli usi legittimi dei puntatori grezzi diventeranno sospetti. Ci sarà una polizia puntatore.

Questo non è solo probabilmente il peggior pericolo, potrebbe essere l’unico serio pericolo. Tutti i peggiori abusi di shared_ptr saranno la diretta conseguenza dell’idea che i puntatori intelligenti siano superiori al puntatore raw (qualunque cosa significhi), e che mettere puntatori intelligenti ovunque renderà la programmazione C ++ “più sicura”.

Naturalmente il semplice fatto che un puntatore intelligente debba essere convertito in un puntatore raw da utilizzare confuta questa affermazione del culto del puntatore intelligente, ma il fatto che l’accesso al puntatore non elaborato sia “implicito” in operator* , operator-> (o esplicito in get() ), ma non implicito in una conversione implicita, è sufficiente per dare l’impressione che questa non sia realmente una conversione e che il puntatore raw prodotto da questa non conversione sia un temporaneo innocuo.

C ++ non può essere reso un “linguaggio sicuro” e nessun sottoinsieme utile di C ++ è “sicuro”

Ovviamente il perseguimento di un sottoinsieme sicuro (“sicuro” nel senso stretto di “memoria sicura”, come LISP, Haskell, Java …) del C ++ è destinato a essere infinito e insoddisfacente, poiché il sottoinsieme sicuro di C ++ è minuscolo e quasi inutile, poiché i primitivi non sicuri sono la regola piuttosto che l’eccezione. La rigorosa sicurezza della memoria in C ++ significherebbe nessun puntatore e solo riferimenti con class di archiviazione automatica . Ma in una lingua in cui il programmatore è considerato affidabile per definizione , alcune persone insistono nell’usare un “puntatore intelligente” (in principio) a prova di idiota, anche laddove non vi è alcun altro vantaggio rispetto ai puntatori grezzi che un modo specifico di avvitare lo stato del programma è evitato.