2015-02-23 8 views
21

Supponiamo di avere un oggetto gestito da un std::unique_ptr. Altre parti del mio codice devono accedere a questo oggetto. Qual è la soluzione giusta per passare il puntatore? Devo semplicemente passare il puntatore semplice per std::unique_ptr::get o devo usare e passare uno std::shared_ptr invece dello std::unique_ptr?Devo usare std :: pointer condiviso per passare un puntatore?

Ho una preferenza per lo std::unique_ptr perché il proprietario di quel puntatore è effettivamente responsabile della pulizia. Se utilizzo un puntatore condiviso, c'è la possibilità che l'oggetto rimanga in vita a causa di un puntatore condiviso anche quando dovrebbe essere effettivamente distrutto.

EDIT: Sfortunatamente, ho dimenticato di menzionare che il puntatore non sarà solo un argomento di una chiamata di funzione, ma verrà memorizzato in altri oggetti per creare una struttura di rete di oggetti. Non preferisco il puntatore condiviso perché non è più chiaro, chi possiede effettivamente l'oggetto.

+1

perché non passare il riferimento dell'oggetto unique_ptr in giro? – Zaiborg

+0

Non si può avere un 'unique_ptr' e un' shared_ptr' gestiscono entrambi lo stesso oggetto. –

+3

passando un riferimento all'oggetto indicato, è una buona idea. –

risposta

18

Se la proprietà dell'oggetto gestito non viene trasferita (e poiché è una unique_ptr, la proprietà non può essere condivisa), allora è più corretto separare la logica nella funzione chiamata dal concetto di proprietà. Lo facciamo chiamando per riferimento.

Questo è un modo contorto per dire:

Data:

std::unique_ptr<Thing> thing_ptr; 

per cambiare la Cosa:

// declaration 
void doSomethingWith(Thing& thing); 

// called like this 
doSomethingWith(*thing_ptr); 

di utilizzare la cosa senza modificarlo.

// declaration 
void doSomethingWith(const Thing& thing); 

// called like this 
doSomethingWith(*thing_ptr); 

L'unica volta che ci si vuole parlare della unique_ptr nella firma funzione sarebbe se si stesse trasferendo la proprietà:

// declaration 
void takeMyThing(std::unique_ptr<Thing> p); 

// call site 
takeMyThing(std::move(thing_ptr)); 

Non hai mai bisogno di fare questo:

void useMyThing(const std::unique_ptr<Thing>& p); 

Il motivo per cui questa sarebbe una cattiva idea è che se confonde la logica di useMyThing con il concetto di proprietà, restringendo così l'ambito di riutilizzo.

considerare:

useMyThing(const Thing& thing); 

Thing x; 
std::unique_ptr<Thing> thing_ptr = makeAThing(); 
useMyThing(x); 
useMyThing(*thing_ptr); 

Aggiornamento:

Notando l'aggiornamento alla domanda - la memorizzazione (non proprietario) i riferimenti a questo oggetto.

Un modo per fare ciò è in effetti memorizzare un puntatore. Tuttavia, i puntatori soffrono della possibilità di un errore logico in quanto possono essere legalmente nulli. Un altro problema con i puntatori è che non funzionano bene con std:: algorithms e contenitori - che richiedono funzioni di confronto personalizzate e simili.

C'è un modo std::-compliant per fare questo - il std::reference_wrapper<>

Quindi, piuttosto che questo:

std::vector<Thing*> my_thing_ptrs; 

fare questo:

std::vector<std::reference_wrapper<Thing>> my_thing_refs; 

Dal std::reference_wrapper<T> definisce un operatore T&, è possibile utilizzare l'oggetto reference_wrapped in qualsiasi espressione che si aspetterebbe da un T.

ad esempio:

std::unique_ptr<Thing> t1 = make_thing(); 
std::unique_ptr<Thing> t2 = make_thing(); 
std::unique_ptr<Thing> t3 = make_thing(); 

std::vector<std::reference_wrapper<const Thing>> thing_cache; 

store_thing(*t1); 
store_thing(*t2); 
store_thing(*t3); 

int total = 0; 
for(const auto& t : thing_cache) { 
    total += value_of_thing(t); 
} 

dove:

void store_thing(const Thing& t) { 
    thing_cache.push_back(std::cref(t)); 
} 

int value_of_thing(const Thing& t) { 
    return <some calculation on t>; 
} 
+0

Questa è un'ottima risposta, grazie mille! Sfortunatamente, ho dimenticato di indicare un fatto importante, che ora ho modificato nel mio post. – Michael

+0

@ Michael Penso che anche se le altre parti del codice desiderano mantenere il riferimento, questo consiglio rimane valido. Finché puoi assicurarti che le altre parti del codice non la useranno dopo che 'unique_ptr' è stato distrutto. Se non è possibile garantire che quindi 'unique_ptr' non sia appropriato. –

6

In genere si passa semplicemente un riferimento o un puntatore semplice ad altre parti del codice che desiderano osservare l'oggetto.

Pass di riferimento:

void func(const Foo& foo); 

std::unique_ptr<Foo> ptr; 

// allocate ptr... 

if(ptr) 
    func(*ptr); 

Pass di puntatore non elaborato:

void func(const Foo* foo); 

std::unique_ptr<Foo> ptr; 

// allocate ptr... 

func(ptr.get()); 

La scelta dipenderà dalla necessità di passare un puntatore nullo.

È responsabilità dell'utente garantire che gli osservatori non utilizzino il puntatore o il riferimento dopo che lo unique_ptr è stato distrutto. Se non è possibile garantire che quindi è necessario utilizzare uno shared_ptr anziché unique_ptr. Gli osservatori possono tenere un weak_ptr per indicare che non hanno la proprietà.

MODIFICA: anche se gli osservatori desiderano mantenere il puntatore o il riferimento che è OK ma rende più difficile assicurarsi che non venga utilizzato dopo che lo unique_ptr è stato distrutto.

+0

Penso che tu tocchi il problema qui con "la tua responsabilità". Tenere i riferimenti a oggetti possibilmente deceduti è un segnale di cattiva comunicazione (la struttura dei dati dovrebbe essere informata che un elemento è stato cancellato). E infatti, un weak_ptr potrebbe essere una soluzione economica. –

3

Dal momento che l'aggiornamento domanda, preferisco:

  1. std::reference_wrapper<> come suggerito da Richard Hodges, a condizione che non è richiesta l'opzione dei valori NULL.

    Nota che il tuo caso d'uso sembra richiedere un costruttore predefinito, quindi questo è fuori a meno che tu non abbia qualche istanza statica pubblica da usare come predefinita.

  2. un po 'di tipo personalizzato not_your_pointer<T> con la semantica richiesta: predefinito-costruibile, copiabile e assegnabile, convertibile da unique_ptr e non proprietario. Probabilmente vale la pena solo se la usi molto, dal momento che scrivere una nuova classe di puntatore intelligente richiede qualche riflessione.

  3. se è necessario gestire penzoloni riferimenti con garbo, utilizzare std::weak_ptr e cambiare la proprietà ad un shared_ptr (Cioè, solo uno shared_ptr esiste: non c'è alcuna ambiguità sulla proprietà e durata degli oggetti è influenzato)


Prima dell'aggiornamento domanda, ho preferito:

  1. indicano che la proprietà non viene trasferita passando un riferimento:

    void foo(T &obj); // no-one expects this to transfer ownership 
    

    chiamato come:

    foo(*ptr); 
    

    si perde la capacità di passare nullptr però, se quello che conta.

  2. indicano che la proprietà non è trasferita vietando esplicitamente:

    void foo(std::unique_ptr<T> const &p) { 
        // can't move or reset p to take ownership 
        // still get non-const access to T 
    } 
    

    questo è chiaro, ma non esporre la vostra politica di gestione della memoria al chiamato.

  3. passare un puntatore non elaborato e documentare che la proprietà non deve essere trasferita. Questo è il più debole e il più soggetto a errori. Non farlo

+0

Cosa offre un 'const unique_ptr &' che un puntatore raw non possiede? E come dici tu, il lato negativo è che espone la tua politica di gestione della memoria? –

+0

const-ref-to-unique_ptr rende il codice esplicitamente documentato e garantisce il contratto. È improbabile che il callee penserà che è necessario (o consentito) ad assumere la proprietà, e dovranno usare 'const_cast' o qualche altro trucco per ottenerlo. E a differenza del riferimento grezzo, puoi ancora passare a 'nullptr'. – Useless

+0

@Utile: Nulla nella specifica 'const unique_ptr &' impedisce di ottenere il puntatore raw sottostante tramite 'p.get()', e di fare ciò che si vuole con quello (l'archiviazione in una struttura dati è ciò che OP intende). E l'ipotesi di default nel passare un puntatore o un riferimento grezzo sarebbe che la proprietà non è trasferita (considerate tutte quelle funzioni che assumono 'const char *'), quindi l'opzione 2 non vi sta davvero comprando molto. Il punto principale è molto chiaramente la documentazione che un puntatore non è posseduto nel luogo in cui viene memorizzato. –

4

Non concentrarsi così tanto sui requisiti del chiamante. In che cosa deve essere trasferita la funzione? Se utilizzerà l'oggetto solo per la durata della chiamata, utilizzare un riferimento.

void func(const Foo& foo); 

Se deve mantenere la proprietà dell'oggetto passato la sua durata, quindi passare in un shared_ptr

void func(std::shared_ptr<Foo> foo); 
+1

L'ultima parte potrebbe essere più chiara dato che la domanda inizia con "Supponiamo che abbia un oggetto che è gestito da uno std :: unique_ptr". –

0

Questo motivo è già stato coperto da Herb Sutter in GotW #91 - Smart Pointer Parameters. Tocca anche l'aspetto delle prestazioni, mentre le altre risposte, benché eccezionali, si concentrano sulla gestione della memoria.

Qualcuno in precedenza ha raccomandato di passare un puntatore intelligente come riferimento const - ha cambiato idea in una modifica successiva. Avevamo un codice in cui questo era abusato - rendeva le firme più intelligenti - cioè più complicate ma senza vantaggi. Il peggio è quando uno sviluppatore junior prende il sopravvento - non metterebbe in dubbio come @ Michael e 'imparare' come farlo da un cattivo esempio. Funziona, ma ...

Per me il parametro del puntatore intelligente che passa la materia è abbastanza serio da essere menzionato in modo evidente in qualsiasi libro/articolo/post immediatamente dopo aver spiegato quali puntatori intelligenti sono.

Chiedendosi ora se i programmi simili a filamenti dovrebbero contrassegnare il passaggio di const-const come elemento di stile.

Problemi correlati