2012-06-30 17 views
54

Ho il mio primo tentativo di utilizzare C++ 11 unique_ptr; Sto sostituendo un puntatore raw polimorfo all'interno di un mio progetto, che è di proprietà di una classe, ma è passato molto spesso.Come passare std :: unique_ptr in giro?

ho usato per avere funzioni come:

bool func(BaseClass* ptr, int other_arg) { 
    bool val; 
    // plain ordinary function that does something... 
    return val; 
} 

Ma presto mi sono reso conto che non sarei stato in grado di passare a:

bool func(std::unique_ptr<BaseClass> ptr, int other_arg); 

perché il chiamante avrebbe dovuto gestire la proprietà puntatore alla funzione, ciò che non voglio. Quindi, qual è la migliore soluzione al mio problema?

ho pensato di passare il puntatore come riferimento, in questo modo:

bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg); 

ma mi sento molto a disagio nel farlo, in primo luogo perché sembra non istintiva di trasmettere qualcosa di già digitato come _ptr come riferimento, quale sarebbe essere un riferimento di un riferimento. In secondo luogo perché la firma della funzione diventa ancora più grande. In terzo luogo, poiché nel codice generato, sarebbero necessari due puntatori indiretti al puntatore per raggiungere la mia variabile.

risposta

69

Se si desidera che la funzione utilizzi il pointee, passare un riferimento ad esso. Non c'è motivo di legare la funzione per funzionare solo con un qualche tipo di puntatore intelligente:

bool func(BaseClass& base, int other_arg); 

E al utilizzo del sito chiamata operator*:

func(*some_unique_ptr, 42); 

In alternativa, se l'argomento base è permesso di essere nullo , mantenere la firma come è, e utilizzare la funzione get() membro:

bool func(BaseClass* base, int other_arg); 
func(some_unique_ptr.get(), 42); 
+1

Questo è bello, ma la cam si libera di 'std :: unique_ptr' per un argomento' std :: vector '? –

17

sono d'accordo con Martinho, ma penso che sia importante t o Indicare la semantica della proprietà di un pass-by-reference. Credo che la soluzione corretta è quella di utilizzare un riferimento di passaggio per semplice qui:

bool func(BaseClass& base, int other_arg); 

Il significato comunemente accettata di un passaggio per riferimento in C++ è come come se il chiamante della funzione indica alla funzione " qui, puoi prendere in prestito questo oggetto, usarlo e modificarlo (se non const), ma solo per la durata del corpo della funzione. " Questo non è in alcun modo in conflitto con le regole di proprietà dello unique_ptr perché l'oggetto viene semplicemente preso in prestito per un breve periodo di tempo, non avviene alcun trasferimento di proprietà effettivo (se presti la tua auto a qualcuno, firmi il titolo su di lui?).

Quindi, anche se potrebbe sembrare sbagliato (design, pratiche di codifica, ecc.) Estrarre il riferimento (o anche il puntatore raw) dallo unique_ptr, in realtà non è perché è perfettamente in accordo con le regole di proprietà impostate dal unique_ptr. E poi, naturalmente, ci sono altri vantaggi, come la sintassi pulita, nessuna limitazione solo agli oggetti di proprietà di uno unique_ptr e così via.

17

Il vantaggio di utilizzare std::unique_ptr<T> (parte non dover ricordare chiamare delete o delete[] esplicitamente) è che garantisce che un puntatore o è nullptr o punta a un'istanza valida dell'oggetto (base).Tornerò su questo dopo aver risposto alla tua domanda, ma il primo messaggio è DO usa puntatori intelligenti per gestire la durata di oggetti allocati dinamicamente.

Ora, il problema è in realtà come utilizzare questo con il vostro vecchio codice.

mio suggerimento è che se non si desidera trasferire o condividere la proprietà, si dovrebbe sempre passa riferimenti all'oggetto. Dichiarare la funzione come questo (con o senza const qualificazioni, se necessario):

bool func(BaseClass& ref, int other_arg) { ... } 

Poi il chiamante, che ha un std::shared_ptr<BaseClass> ptr sarà o gestire il caso nullptr oppure chiederà bool func(...) per calcolare il risultato:

if (ptr) { 
    result = func(*ptr, some_int); 
} else { 
    /* the object was, for some reason, either not created or destroyed */ 
} 

Ciò significa che qualsiasi chiamante deve promettere che il riferimento è valido e che continuerà a essere valido durante l'esecuzione del corpo della funzione.


Ecco il motivo per cui credo fermamente si dovrebbe non passaggio puntatori prime o riferimenti a puntatori intelligenti.

Un puntatore non elaborato è solo un indirizzo di memoria. Può avere uno dei (almeno) 4 significati:

  1. L'indirizzo di un blocco di memoria in cui si trova l'oggetto desiderato. (il buono)
  2. L'indirizzo 0x0 di cui si può essere certi non è dereferenziabile e potrebbe avere la semantica di "niente" o "nessun oggetto". (the bad)
  3. L'indirizzo di un blocco di memoria che si trova al di fuori dello spazio indirizzabile del processo (il dereferenziamento potrebbe causare l'arresto anomalo del programma). (il brutto)
  4. L'indirizzo di un blocco di memoria che può essere dereferenziato ma che non contiene ciò che ci si aspetta. Forse il puntatore è stato modificato per errore e ora punta a un altro indirizzo scrivibile (di una variabile completamente diversa all'interno del processo). Scrivere in questa posizione di memoria farà sì che si verifichi un sacco di divertimento, a volte, durante l'esecuzione, perché il sistema operativo non si lamenterà fino a quando ti è permesso di scrivere lì. (Zoinks!)

correttamente utilizzando puntatori intelligenti allevia i casi piuttosto spaventosi 3 e 4, che di solito non sono rilevabili in fase di compilazione, e che in genere solo l'esperienza in fase di esecuzione quando il tuo programma va in crash o fa cose inaspettate.

Passando puntatori intelligenti come argomenti ha due svantaggi: non è possibile modificare la const -ness del sottolineato oggetto senza fare una copia (che aggiunge overhead per shared_ptr e non è possibile per unique_ptr), e vi sono ancora a sinistra con il secondo significato (nullptr).

Ho contrassegnato il secondo caso come (il cattivo) dal punto di vista del design. Questa è una discussione più sottile sulla responsabilità.

Immaginate cosa significa quando una funzione riceve un parametro come nullptr. Prima deve decidere cosa farne: usare un valore "magico" al posto dell'oggetto mancante? cambiare completamente il comportamento e calcolare qualcos'altro (che non richiede l'oggetto)? prendere dal panico e fare un'eccezione? Inoltre, cosa succede quando la funzione prende 2, o 3 o anche più argomenti con il puntatore raw? Deve controllare ciascuno di essi e adattare il suo comportamento di conseguenza. Questo aggiunge un nuovo livello alla convalida dell'input senza un vero motivo.

Il chiamante dovrebbe essere quello con sufficienti informazioni contestuali per prendere queste decisioni, o, in altre parole, il male è meno spaventoso più si sa. La funzione, d'altra parte, dovrebbe solo prendere la promessa del chiamante che la memoria a cui è puntata è sicura per funzionare come previsto. (I riferimenti sono ancora indirizzi di memoria, ma concettualmente rappresentano una promessa di validità.)

0

Personalmente, evito di estrarre un riferimento da un puntatore/puntatore intelligente. Perché cosa succede se il puntatore è nullptr? Se si modifica la firma a questo:

bool func(BaseClass& base, int other_arg); 

Potrebbe essere necessario proteggere il codice da nulli dereferenziazioni di puntatori:

if (the_unique_ptr) 
    func(*the_unique_ptr, 10); 

Se la classe è l'unico proprietario del puntatore, il secondo dei alternativa di Martinho sembra più ragionevole:

func(the_unique_ptr.get(), 10); 

In alternativa, è possibile utilizzare std::shared_ptr. Tuttavia, se esiste una sola entità responsabile per delete, l'overhead std::shared_ptr non paga.

+0

Sei consapevole che, nella maggior parte delle occasioni, 'std :: unique_ptr' ha zero overhead, giusto? – lvella

+0

Sì, ne sono consapevole. Ecco perché gli ho parlato del sovraccarico di 'std :: shared_ptr'. –

Problemi correlati