Come restituire i puntatori intelligenti (shared_ptr), per riferimento o per valore?


94

Diciamo che ho una classe con un metodo che restituisce un file shared_ptr.

Quali sono i possibili vantaggi e svantaggi della restituzione per riferimento o per valore?

Due possibili indizi:

  • Distruzione precoce degli oggetti. Se restituisco il riferimento shared_ptrby (const), il contatore dei riferimenti non viene incrementato, quindi corro il rischio di avere l'oggetto cancellato quando esce dall'ambito in un altro contesto (ad esempio un altro thread). È corretto? E se l'ambiente fosse a thread singolo, può verificarsi anche questa situazione?
  • Costo. Il pass-by-value non è certamente gratuito. Vale la pena evitarlo quando possibile?

Grazie a tutti.

Risposte:


114

Restituisce puntatori intelligenti per valore.

Come hai detto, se lo restituisci per riferimento, non aumenterai correttamente il conteggio dei riferimenti, il che espone al rischio di eliminare qualcosa al momento sbagliato. Questo da solo dovrebbe essere un motivo sufficiente per non tornare per riferimento. Le interfacce dovrebbero essere robuste.

La preoccupazione dei costi è oggigiorno discutibile grazie all'ottimizzazione del valore di ritorno (RVO), quindi non incorrerai in una sequenza di incremento-incremento-decremento o qualcosa del genere nei compilatori moderni. Quindi il modo migliore per restituire a shared_ptrè semplicemente restituire per valore:

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

Questa è un'ovvia opportunità RVO per i moderni compilatori C ++. So per certo che i compilatori Visual C ++ implementano RVO anche quando tutte le ottimizzazioni sono disattivate. E con la semantica di spostamento di C ++ 11, questa preoccupazione è ancora meno rilevante. (Ma l'unico modo per essere sicuri è profilare e sperimentare.)

Se non sei ancora convinto, Dave Abrahams ha un articolo che sostiene il ritorno in base al valore. Riporto qui uno snippet; Consiglio vivamente di leggere l'intero articolo:

Sii onesto: come ti fa sentire il codice seguente?

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

Francamente, anche se dovrei saperlo meglio, mi rende nervoso. In linea di principio, quando get_names() ritorna, dobbiamo copiare a vectordi strings. Quindi, dobbiamo copiarlo di nuovo quando inizializziamo namese dobbiamo distruggere la prima copia. Se ci sono N stringnel vettore, ogni copia potrebbe richiedere fino a N + 1 allocazioni di memoria e tutta una serie di accessi ai dati non compatibili con la cache> man mano che il contenuto della stringa viene copiato.

Piuttosto che affrontare quel tipo di ansia, sono spesso tornato sul pass-by-reference per evitare copie inutili:

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 la constquestione perché stiamo mutando i nomi.
  • Come piace ricordarci ai programmatori funzionali, la mutazione rende il codice più complesso da ragionare minando la trasparenza referenziale e il ragionamento equazionale.
  • Non abbiamo più una semantica di valori rigidi per i nomi.

Ma è davvero necessario rovinare il nostro codice in questo modo per guadagnare efficienza? Fortunatamente, la risposta risulta essere no (e soprattutto non se stai usando C ++ 0x).


Non so se direi che RVO rende la questione discutibile poiché tornare per riferimento rende decisamente impossibile RVO.
Edward Strange

@ CrazyEddie: Vero, questo è uno dei motivi per cui raccomando che l'OP restituisca per valore.
In silico

La regola RVO, consentita dallo standard, vince le regole sulla sincronizzazione / relazioni accade prima, garantite dallo standard?
edA-qa mort-ora-y

1
@ edA-qa mort-ora-y: RVO è esplicitamente consentito anche se ha effetti collaterali. Ad esempio, se hai cout << "Hello World!";un'istruzione in un costruttore predefinito e copia, non vedrai due Hello World!s quando RVO è in vigore. Tuttavia, questo non dovrebbe essere un problema per i puntatori intelligenti progettati correttamente, anche per la sincronizzazione.
In silico

23

Per quanto riguarda qualsiasi puntatore intelligente (non solo shared_ptr), non penso sia mai accettabile restituire un riferimento a uno, e sarei molto riluttante a passarli in giro per riferimento o puntatore grezzo. Perché? Perché non puoi essere certo che non verrà copiato in modo superficiale tramite un riferimento in seguito. Il tuo primo punto definisce il motivo per cui questo dovrebbe essere un problema. Ciò può accadere anche in un ambiente a thread singolo. Non è necessario l'accesso simultaneo ai dati per inserire la semantica della copia errata nei programmi. Non puoi davvero controllare ciò che i tuoi utenti fanno con il puntatore una volta che lo fai passare, quindi non incoraggiare l'uso improprio dando ai tuoi utenti API abbastanza corda per impiccarsi.

In secondo luogo, guarda l'implementazione del tuo puntatore intelligente, se possibile. La costruzione e la distruzione dovrebbero essere dannatamente quasi trascurabili. Se questo sovraccarico non è accettabile, non utilizzare un puntatore intelligente! Ma oltre a questo, dovrai anche esaminare l'architettura di concorrenza che hai, perché l'accesso mutuamente 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 ++, modificherei la mia risposta per accettare maggiormente i casi in cui hai semplicemente scritto un lambda che non vive mai al di fuori dell'ambito della funzione chiamante e non lo è copiato da qualche altra parte. In questo caso, se si desidera salvare l'overhead minimo di copiare un puntatore condiviso, sarebbe giusto e sicuro. Perché? Perché puoi garantire che il riferimento non verrà mai utilizzato in modo improprio.

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.