2012-05-17 19 views
61

Diciamo che ho una classe con un metodo che restituisce uno shared_ptr.Come restituire puntatori intelligenti (shared_ptr), per riferimento o per valore?

Quali sono i possibili vantaggi e gli svantaggi di restituirlo per riferimento o per valore?

Due possibili indizi:

  • precoce distruzione oggetto. Se restituisco shared_ptr tramite il riferimento (const), il contatore di riferimento non viene incrementato, pertanto rischio di avere l'oggetto eliminato quando esce dall'ambito in un altro contesto (ad esempio un altro thread). È corretto? Cosa succede se l'ambiente è a thread singolo, può succedere anche questa situazione?
  • Costo. Il pass-by-value non è certamente gratuito. Vale la pena di evitarlo quando è possibile?

Grazie a tutti.

risposta

80

Restituisce i puntatori intelligenti in base al valore.

Come hai detto, se lo si restituisce per riferimento, non si incrementerà correttamente il conteggio dei riferimenti, il che apre il rischio di eliminare qualcosa in un momento improprio. Solo questo dovrebbe essere una ragione sufficiente per non tornare per riferimento. Le interfacce dovrebbero essere robuste.

La preoccupazione di costo è oggi discutibile grazie a return value optimization (RVO), quindi non si incorre in una sequenza di incremento/decremento incrementale o qualcosa di simile nei moderni compilatori. Quindi il modo migliore per restituire un shared_ptr è quello di restituire semplicemente il valore:

shared_ptr<T> Foo() 
{ 
    return shared_ptr<T>(/* acquire something */); 
}; 

Si tratta di un'occasione RVO morto ovvio per i moderni compilatori C++. So per certo che i compilatori di Visual C++ implementano RVO anche quando tutte le ottimizzazioni sono disattivate. E con la semantica del movimento di C++ 11, questa preoccupazione è ancora meno rilevante. (Ma l'unico modo per essere sicuri è profilare ed esperire.)

Se non sei ancora convinto, Dave Abrahams ha an article che fa un argomento per il ritorno di valore. Riproduco un frammento qui; Consiglio vivamente di leggere l'intero articolo:

Sii sincero: come ti fa sentire il seguente codice?

std::vector<std::string> get_names(); 
... 
std::vector<std::string> const names = get_names(); 

Francamente, anche se dovrei sapere meglio, mi rende nervoso. In linea di principio, quando ritorna il get_names() , dobbiamo copiare uno vector di string s. Quindi, abbiamo bisogno di copiarlo di nuovo quando inizializziamo names, e abbiamo bisogno di distruggere la prima copia. Se nel vettore sono presenti N string s, ciascuna copia potrebbe richiedere un numero di allocazioni di memoria N + 1 e un'intera serie di accessi ai dati cache-non amichevoli> quando i contenuti della stringa vengono copiati.

Invece di confrontarsi con quella specie di ansia, ho spesso ripiegare su pass-by-di riferimento al fine di evitare inutili copie:

get_names(std::vector<std::string>& out_param); 
... 
std::vector<std::string> names; 
get_names(names); 

Sfortunatamente, questo approccio è tutt'altro che ideale.

  • Il codice è cresciuto del 150%
  • Abbiamo dovuto abbandonare const -ness perché stiamo mutando nomi.
  • Come i programmatori funzionali amano ricordarci, la mutazione rende il codice più complesso da ragionare minando la trasparenza referenziale e il ragionamento equo.
  • Non abbiamo più la semantica del valore stretto per i nomi.

Ma è davvero necessario incasinare il nostro codice in questo modo per ottenere efficienza? Fortunatamente, la risposta risulta essere no (e specialmente non se si utilizza C++ 0x).

+0

Non so che direi RVO rende la domanda discutibile dal momento che il rinvio tramite riferimento rende decisamente impossibile il RVO. –

+0

@CrazyEddie: true, questo è uno dei motivi per cui consiglio di restituire l'OP in base al valore. –

+0

La regola RVO, consentita dallo standard, supera le regole sulle relazioni di sincronizzazione/accade-prima, garantite dallo standard? –

17

Per quanto riguarda qualsiasi puntatore intelligente (non solo shared_ptr), non credo che sia mai accettabile per restituire un riferimento a uno, e sarei molto riluttanti a passare in giro per riferimento o puntatore crudo. Perché? Perché non puoi essere certo che non verrà copiato superficialmente tramite un riferimento più tardi. Il tuo primo punto definisce la ragione per cui questa dovrebbe essere una preoccupazione. Questo può accadere anche in un ambiente a thread singolo. Non è necessario l'accesso simultaneo ai dati per inserire la semantica delle copie errate nei programmi. Non controlli realmente ciò che i tuoi utenti fanno con il puntatore quando lo passi, quindi non incoraggiare l'uso improprio che dà agli utenti dell'API una corda sufficiente per impiccarsi.

In secondo luogo, esaminare l'implementazione del puntatore intelligente, se possibile. Costruzione e distruzione dovrebbero essere dannatamente vicini a trascurabili. Se questo sovraccarico non è accettabile, non utilizzare un puntatore intelligente! Ma oltre a questo, dovrai anche esaminare l'architettura della concorrenza che hai, perché l'accesso reciprocamente esclusivo al meccanismo che tiene traccia degli usi del puntatore ti rallenterà più della semplice costruzione dell'oggetto shared_ptr.

Modifica, 3 anni dopo: con l'avvento delle funzionalità più moderne in C++, vorrei modificare la mia risposta per essere più accettando i casi in cui hai semplicemente scritto un lambda che non vive mai al di fuori dell'ambito della funzione chiamante, e non è stato copiato da qualche altra parte. Qui, se si volesse risparmiare l'onere minimo di copiare un puntatore condiviso, sarebbe giusto e sicuro. Perché? Perché è possibile garantire che il riferimento non verrà mai utilizzato in modo errato.

Problemi correlati