Che tipo di puntatore devo usare quando?


228

Ok, quindi l'ultima volta che ho scritto C ++ per vivere, std::auto_ptrc'era tutta la lib standard disponibile ed boost::shared_ptrera di gran moda. Non ho mai veramente esaminato gli altri tipi di puntatori intelligenti forniti. Comprendo che C ++ 11 ora fornisce alcuni dei tipi di boost creati, ma non tutti.

Quindi qualcuno ha un semplice algoritmo per determinare quando utilizzare quale puntatore intelligente? Preferibilmente includendo consigli su puntatori stupidi (puntatori grezzi come T*) e il resto dei puntatori intelligenti boost. (Qualcosa di simile a questo sarebbe grande).



1
Spero davvero che qualcuno realizzi un diagramma di flusso utile come questo diagramma di flusso di selezione STL .
Risparmia il

1
@Als: Oh, è davvero bello! L'ho FAQ.
sabato

6
@Deduplicator Non è nemmeno vicino ad essere un duplicato. La domanda collegata dice "Quando devo usare un puntatore intelligente" e questa domanda è "Quando devo usare questi puntatori intelligenti?" cioè questo sta classificando i diversi usi dei puntatori intelligenti standard. La domanda collegata non lo fa. La differenza è apparentemente piccola ma è grande.
Rapptz,

Risposte:


183

Proprietà condivisa:
L' shared_ptre weak_ptrlo standard adottato sono praticamente la stessa come le loro controparti Boost . Usali quando devi condividere una risorsa e non sai quale sarà l'ultima ad essere viva. Utilizzare weak_ptrper osservare la risorsa condivisa senza influenzarne la durata, non per interrompere i cicli. I cicli con shared_ptrnormalmente non dovrebbero accadere: due risorse non possono possedere l'un l'altro.

Nota che Boost offre inoltre shared_array, che potrebbe essere un'alternativa adatta a shared_ptr<std::vector<T> const>.

Successivamente, Boost offre intrusive_ptr, che sono una soluzione leggera se la tua risorsa offre già una gestione conteggio dei riferimenti e desideri adottarla secondo il principio RAII. Questo non è stato adottato dalla norma.

Proprietà unica:
Boost ha anche a scoped_ptr, che non è copiabile e per il quale non è possibile specificare un deleter. std::unique_ptrè boost::scoped_ptrsugli steroidi e dovrebbe essere la scelta predefinita quando hai bisogno di un puntatore intelligente . Ti permette di specificare un deleter nei suoi argomenti template ed è mobile , a differenza boost::scoped_ptr. È inoltre completamente utilizzabile nei contenitori STL purché non si utilizzino operazioni che richiedono tipi di copia (ovviamente).

Notare ancora, che Boost ha una versione array:, scoped_arrayche lo standard ha unificato richiedendo std::unique_ptr<T[]>una specializzazione parziale che farà da delete[]puntatore invece di deleteing (con la default_deleter). std::unique_ptr<T[]>offre anche operator[]invece di operator*e operator->.

Si noti che std::auto_ptrè ancora nello standard, ma è obsoleto . §D.10 [depr.auto.ptr]

Il modello di classe auto_ptrè obsoleto. [ Nota: il modello di classe unique_ptr(20.7.1) fornisce una soluzione migliore. —Endola nota ]

Nessuna proprietà:
utilizzare puntatori stupidi (puntatori non elaborati) o riferimenti per riferimenti non proprietari alle risorse e quando si sa che la risorsa sopravviverà all'oggetto / ambito di riferimento. Preferisci i riferimenti e usa i puntatori non elaborati quando hai bisogno di nullability o ripristinabilità.

Se si desidera un riferimento non proprietario a una risorsa, ma non si sa se la risorsa sopravviverà all'oggetto a cui fa riferimento, comprimere la risorsa in a shared_ptre utilizzare a weak_ptr- è possibile verificare se il genitore shared_ptrè vivo con lock, il che farà restituisce un valore shared_ptrnon nullo se la risorsa esiste ancora. Se si desidera verificare se la risorsa è morta, utilizzare expired. I due possono sembrare simili, ma sono molto diversi rispetto all'esecuzione simultanea, poiché expiredgarantisce solo il suo valore di ritorno per quella singola istruzione. Un test apparentemente innocente come

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

è una potenziale condizione di razza.


1
In caso di assenza di proprietà, è consigliabile preferire i riferimenti ai puntatori, a meno che non siano necessari proprietà e ripristinabilità laddove i riferimenti non lo tagliano, anche se si potrebbe voler considerare di riscrivere l'oggetto originale come un shared_ptre il puntatore non proprietario come a weak_ptr...
David Rodríguez - dribeas

2
Non intendevo riferimento al puntatore , ma piuttosto riferimento al puntatore. Se non esiste alcuna proprietà, a meno che non sia necessaria la ripristinabilità (o la nullità, ma la nullità senza poter reimpostare sarebbe piuttosto limitata) è possibile utilizzare un riferimento semplice anziché un puntatore in primo luogo.
David Rodríguez - dribeas,

1
@ David: Ah, capisco. :) Sì, i riferimenti non sono male per questo, personalmente li preferisco anche in questi casi. Li aggiungerò.
Xeo

1
@Xeo: shared_array<T>è un'alternativa a shared_ptr<T[]>non farlo shared_ptr<vector<T>>: non può crescere.
R. Martinho Fernandes,

1
@GregroyCurrie: Questo è ... esattamente quello che ho scritto? Ho detto che è un esempio di una potenziale condizione di gara.
Xeo,

127

Decidere quale puntatore intelligente utilizzare è una questione di proprietà . Quando si tratta di gestione delle risorse, l'oggetto A possiede l' oggetto B se controlla la durata dell'oggetto B. Ad esempio, le variabili membro sono di proprietà dei rispettivi oggetti perché la durata delle variabili membro è legata alla durata dell'oggetto. Scegli i puntatori intelligenti in base alla proprietà dell'oggetto.

Si noti che la proprietà in un sistema software è separata dalla proprietà come la penseremmo al di fuori del software. Ad esempio, una persona potrebbe "possedere" la propria casa, ma ciò non significa necessariamente che un Personoggetto abbia il controllo sulla durata di un Houseoggetto. Confondere questi concetti del mondo reale con i concetti del software è un modo sicuro per programmare te stesso in un buco.


Se si possiede la proprietà esclusiva dell'oggetto, utilizzare std::unique_ptr<T>.

Se hai la proprietà condivisa dell'oggetto ...
- Se non ci sono cicli nella proprietà, usa std::shared_ptr<T>.
- Se ci sono cicli, definire una "direzione" e utilizzare std::shared_ptr<T>in una direzione e std::weak_ptr<T>nell'altra.

Se l'oggetto ti possiede, ma esiste il potenziale di non avere proprietario, usa i normali puntatori T*(ad esempio i puntatori principali).

Se l'oggetto ti possiede (o comunque ha un'esistenza garantita), usa i riferimenti T&.


Avvertenza: prestare attenzione ai costi dei puntatori intelligenti. In ambienti con memoria o prestazioni limitate, potrebbe essere utile utilizzare solo normali puntatori con uno schema più manuale per la gestione della memoria.

I costi:

  • Se si dispone di un deleter personalizzato (ad es. Si utilizzano pool di allocazione), ciò comporterà un sovraccarico per puntatore che può essere facilmente evitato con l'eliminazione manuale.
  • std::shared_ptrha l'overhead di un incremento del conteggio dei riferimenti sulla copia, oltre a un decremento sulla distruzione seguito da un controllo del conteggio 0 con cancellazione dell'oggetto trattenuto. A seconda dell'implementazione, questo può gonfiare il codice e causare problemi di prestazioni.
  • Tempo di compilazione. Come per tutti i modelli, i puntatori intelligenti contribuiscono negativamente alla compilazione dei tempi.

Esempi:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Un albero binario non possiede il suo genitore, ma l'esistenza di un albero implica l'esistenza del suo genitore (o nullptrcome root), quindi utilizza un normale puntatore. Un albero binario (con semantica di valore) ha la proprietà esclusiva dei suoi figli, quindi quelli lo sono std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Qui, il nodo elenco possiede i suoi elenchi successivo e precedente, quindi definiamo una direzione e utilizziamo shared_ptrper next e weak_ptrfor prev per interrompere il ciclo.


3
Per l'esempio dell'albero binario alcune persone suggerirebbero di usare shared_ptr<BinaryTree>per i figli e weak_ptr<BinaryTree>per la relazione dei genitori.
David Rodríguez - dribeas,

@ DavidRodríguez-dribeas: dipende se l'albero ha valore semantico o no. Se le persone faranno riferimento al tuo albero esternamente anche una volta che l'albero di origine viene distrutto, sì, la combinazione di puntatori condivisi / deboli sarebbe la cosa migliore.
Peter Alexander,

Se un oggetto ti possiede ed è garantito l'esistenza allora perché non un riferimento.
Martin York,

1
Se usi il riferimento, non puoi mai cambiare il genitore, il che potrebbe ostacolare o meno il disegno. Per bilanciare gli alberi, ciò ostacolerebbe.
Mooing Duck

3
+1 ma dovresti aggiungere una definizione di "proprietà" sulla prima riga. Mi trovo spesso a dover dichiarare chiaramente che si tratta della vita e della morte dell'oggetto, non della proprietà in un significato più specifico del dominio.
Klaim

19

Utilizzare unique_ptr<T>tutto il tempo tranne quando è necessario il conteggio dei riferimenti, nel qual caso utilizzare shared_ptr<T>(e per casi molto rari, weak_ptr<T>per evitare cicli di riferimento). In quasi tutti i casi, la proprietà unica trasferibile va bene.

Puntatori non elaborati: buono solo se sono necessari ritorni covarianti, puntamento non proprietario che può accadere. Altrimenti non sono terribilmente utili.

Puntatori a matrice: unique_ptrdispone di una specializzazione per la T[]quale viene automaticamente richiamato delete[]il risultato, unique_ptr<int[]> p(new int[42]);ad esempio in modo sicuro . shared_ptravresti comunque bisogno di un deleter personalizzato, ma non avresti bisogno di un puntatore a matrice unico o condiviso specializzato. Naturalmente, tali cose sono generalmente meglio sostituite da std::vectorcomunque. Sfortunatamente shared_ptrnon fornisce una funzione di accesso all'array, quindi dovresti comunque chiamare manualmente get(), ma unique_ptr<T[]>fornisce operator[]invece di operator*e operator->. In ogni caso, devi controllare da solo. Questo rende shared_ptrleggermente meno user-friendly, anche se senza dubbio il vantaggio generico e nessuna dipendenza Boost rende unique_ptre ancora shared_ptri vincitori.

Puntatori con ambito: resi irrilevanti da unique_ptr, proprio come auto_ptr.

Non c'è davvero altro. In C ++ 03 senza spostare la semantica questa situazione era molto complicata, ma in C ++ 11 il consiglio è molto semplice.

Ci sono ancora usi per altri puntatori intelligenti, come intrusive_ptro interprocess_ptr. Tuttavia, sono molto di nicchia e completamente inutili nel caso generale.


Inoltre, puntatori non elaborati per l'iterazione. E per i buffer dei parametri di output, in cui il buffer è di proprietà del chiamante.
Ben Voigt,

Hmm, nel modo in cui l'ho letto, sono situazioni che sono sia ritorno covariante che non possesso. Una riscrittura potrebbe essere buona se intendevi l'unione anziché l'intersezione. Direi anche che l'iterazione merita una menzione speciale.
Ben Voigt,

2
std::unique_ptr<T[]>fornisce operator[]invece di operator*e operator->. È vero che devi comunque fare un controllo diretto.
Xeo

8

Casi di quando usare unique_ptr:

  • Metodi di fabbrica
  • Membri che sono puntatori (incluso pimpl)
  • Memorizzare i puntatori nei contenitori stl (per evitare spostamenti)
  • Uso di oggetti dinamici locali di grandi dimensioni

Casi di quando usare shared_ptr:

  • Condivisione di oggetti tra thread
  • Condivisione di oggetti in generale

Casi di quando usare weak_ptr:

  • Grande mappa che funge da riferimento generale (es. Una mappa di tutti i socket aperti)

Sentiti libero di modificare e aggiungere altro


In realtà mi piace di più la tua risposta mentre dai gli scenari.
Nicholas Humphrey,
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.