Dovremmo passare un shared_ptr per riferimento o per valore?


270

Quando una funzione prende un shared_ptr(da boost o C ++ 11 STL), la stai passando:

  • per riferimento const: void foo(const shared_ptr<T>& p)

  • o per valore void foo(shared_ptr<T> p):?

Preferirei il primo metodo perché sospetto che sarebbe più veloce. Ma ne vale davvero la pena o ci sono altri problemi?

Potresti per favore fornire i motivi della tua scelta o, nel caso, perché pensi che non abbia importanza.


14
Il problema è che non sono equivalenti. La versione di riferimento urla "Vado ad alias un po ' shared_ptr, e posso cambiarlo se voglio.", Mentre la versione del valore dice "Ho intenzione di copiare il tuo shared_ptr, quindi mentre posso cambiarlo non lo saprai mai. ) Un parametro const-reference è la vera soluzione, che dice "Vado ad alias alcuni shared_ptr, e prometto di non cambiarlo." (Che è estremamente simile alla semantica per valore!)
GManNickG

2
Ehi, sarei interessato all'opinione dei tuoi ragazzi sul ritorno di un shared_ptrmembro della classe. Lo fai con const-refs?
Johannes Schaub - litb

La terza possibilità è quella di utilizzare std :: move () con C ++ 0x, questo scambia sia shared_ptr
Tomaka17

@Johannes: lo restituirei per riferimento const solo per evitare qualsiasi copia / conteggio dei riferimenti. Poi di nuovo, restituisco tutti i membri per riferimento const a meno che non siano primitivi.
GManNickG,

Risposte:


229

Questa domanda è stata discussa e risposta da Scott, Andrei ed Herb durante la sessione Ask Us Anything a C ++ e Beyond 2011 . Guarda da 4:34 su shared_ptrprestazioni e correttezza .

In breve, non vi è alcun motivo per passare per valore, a meno che l'obiettivo non sia condividere la proprietà di un oggetto (ad es. Tra diverse strutture di dati o tra thread diversi).

A meno che non sia possibile spostarlo, ottimizzarlo come spiegato da Scott Meyers nel video di talk collegato sopra, ma che è correlato alla versione effettiva di C ++ che è possibile utilizzare.

Un importante aggiornamento di questa discussione è avvenuto durante il pannello interattivo della conferenza GoingNative 2012 : Ask Us Anything! che vale la pena guardare, soprattutto dalle 22:50 .


5
ma come mostrato qui è più economico passare per valore: stackoverflow.com/a/12002668/128384 non dovrebbe essere preso in considerazione anche (almeno per gli argomenti del costruttore ecc. dove a shared_ptr diventerà membro di la classe)?
Stijn

2
@stijn Sì e no. Le domande e risposte che indichi sono incomplete, a meno che non chiariscano la versione dello standard C ++ a cui si riferisce. È molto facile diffondere regole generali mai / sempre semplicemente fuorvianti. A meno che, i lettori non impieghino il tempo per familiarizzare con l'articolo e i riferimenti di David Abrahams, o prendere in considerazione la data di pubblicazione rispetto allo standard C ++ corrente. Quindi, entrambe le risposte, la mia e quella che hai indicato, sono corrette dato il momento della pubblicazione.
mloskot

1
"a meno che non ci sia multi-threading " no, MT non è in alcun modo speciale.
curioso

3
Sono in ritardo alla festa, ma la mia ragione per voler condividere shared_ptr per valore è che rende il codice più breve e più carino. Sul serio. Value*è breve e leggibile, ma è male, quindi ora il mio codice è pieno const shared_ptr<Value>&ed è significativamente meno leggibile e solo ... meno ordinato. Quello che era void Function(Value* v1, Value* v2, Value* v3)adesso è void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), e la gente è d'accordo con questo?
Alex,

7
@Alex La pratica comune è creare alias (typedefs) subito dopo la lezione. Per il tuo esempio: la class Value {...}; using ValuePtr = std::shared_ptr<Value>;tua funzione diventa più semplice: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)e ottieni le massime prestazioni. Ecco perché usi C ++, no? :)
4LegsDrivenCat

92

Ecco la versione di Herb Sutter

Linea guida: non passare un puntatore intelligente come parametro di funzione a meno che non si desideri utilizzare o manipolare il puntatore intelligente stesso, ad esempio per condividere o trasferire la proprietà.

Linea guida: esprimere che una funzione memorizzerà e condividerà la proprietà di un oggetto heap utilizzando un parametro condiviso_ptr per valore.

Linea guida: utilizzare un parametro_ptr non const & solo per modificare shared_ptr. Usa const const_ptr & come parametro solo se non sei sicuro di prendere o meno una copia e condividere la proprietà; altrimenti usa invece il widget * (o, se non è nullable, un widget &).


3
Grazie per il link a Sutter. È un articolo eccellente. Non sono d'accordo con lui sul widget *, preferendo <widget &> opzionale se C ++ 14 è disponibile. widget * è troppo ambiguo dal vecchio codice.
Eponimo

3
+1 per includere widget * e widget e come possibilità. Solo per elaborare, passare widget * o widget e probabilmente è l'opzione migliore quando la funzione non sta esaminando / modificando l'oggetto puntatore stesso. L'interfaccia è più generale, in quanto non richiede un tipo di puntatore specifico e viene evitato il problema di prestazioni del conteggio dei riferimenti shared_ptr.
Tgnottingham,

4
Penso che questa dovrebbe essere la risposta accettata oggi, a causa della seconda linea guida. Invalida chiaramente l'attuale risposta accettata, che dice: non c'è motivo di passare per valore.
martedì

62

Personalmente userei un constriferimento. Non è necessario incrementare il conteggio dei riferimenti solo per ridurlo di nuovo a causa di una chiamata di funzione.


1
Non ho votato in negativo la tua risposta, ma prima che questa sia una questione di preferenza, ci sono pro e contro in ciascuna delle due possibilità da considerare. E sarebbe bene conoscere e discutere questi pro e contro. Successivamente tutti possono prendere una decisione da soli.
Danvil,

@Danvil: tenendo conto di come shared_ptrfunziona, l'unico aspetto negativo possibile non passare per riferimento è una leggera perdita di prestazioni. Ci sono due cause qui. a) la funzione di alias puntatore significa che i puntatori valgono i dati più un contatore (forse 2 per riferimenti deboli) viene copiato, quindi è leggermente più costoso copiare i dati in tondo. b) il conteggio dei riferimenti atomici è leggermente più lento del semplice vecchio codice di incremento / decremento, ma è necessario per essere thread-safe. Oltre a ciò, i due metodi sono gli stessi per la maggior parte degli scopi.
Evan Teran,

37

Passa per constriferimento, è più veloce. Se è necessario conservarlo, dire in un contenitore, il rif. il conteggio verrà incrementato automaticamente magicamente dall'operazione di copia.


4
Il downvote è dovuto alla sua opinione senza numeri a sostegno.
kwesolowski,

22

Ho eseguito il codice qui sotto, una volta con fooprendendo il shared_ptrby const&e ancora con il fooprendendo shared_ptrdal valore.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Usando VS2015, build di rilascio x86, sul mio processore Intel Core 2 Quad (2,4 GHz)

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

La copia per versione del valore era un ordine di grandezza più lento.
Se stai chiamando una funzione in modo sincrono dal thread corrente, preferisci la const&versione.


1
Puoi dire quali impostazioni di compilatore, piattaforma e ottimizzazione hai usato?
Carlton,

Ho usato la build di debug di vs2015, ho aggiornato la risposta per utilizzare la build di rilascio ora.
TCB

1
Sono curioso di sapere se quando si attiva l'ottimizzazione, si ottengono gli stessi risultati con entrambi
Elliot Woods,

2
L'ottimizzazione non aiuta molto. il problema è la contesa di blocco sul conteggio dei riferimenti sulla copia.
Alex

1
Non è questo il punto. Una tale foo()funzione non dovrebbe nemmeno accettare un puntatore condiviso in primo luogo perché non utilizza questo oggetto: dovrebbe accettare a int&e do p = ++x;, chiamando foo(*p);da main(). Una funzione accetta un oggetto puntatore intelligente quando deve fare qualcosa con esso e, il più delle volte, ciò che devi fare è spostarlo ( std::move()) da qualche altra parte, quindi un parametro per valore non ha alcun costo.
eepp,

15

Dal C ++ 11 dovresti prenderlo per valore su const e più spesso di quanto potresti pensare.

Se stai prendendo lo std :: shared_ptr (piuttosto che il tipo T sottostante), allora lo stai facendo perché vuoi farci qualcosa.

Se vuoi copiarlo da qualche parte, ha più senso prenderlo per copia e std :: spostalo internamente, piuttosto che prenderlo da const e poi copiarlo. Questo perché si consente al chiamante la possibilità di attivare a sua volta std :: move shared_ptr quando si chiama la propria funzione, salvando così una serie di operazioni di incremento e decremento. O no. Cioè, il chiamante della funzione può decidere se ha bisogno o meno dello std :: shared_ptr dopo aver chiamato la funzione e in base al fatto che si sposti o meno. Ciò non è realizzabile se si passa da const &, quindi è preferibile prenderlo per valore.

Ovviamente, se il chiamante ha bisogno del suo shared_ptr in giro più a lungo (quindi non può std :: spostarlo) e non si desidera creare una copia semplice nella funzione (dire che si desidera un puntatore debole o che a volte si desidera per copiarlo, a seconda di alcune condizioni), quindi una const e potrebbe essere ancora preferibile.

Ad esempio, dovresti farlo

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

al di sopra di

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Perché in questo caso crei sempre una copia internamente


1

Non conoscendo il costo del tempo dell'operazione di copia shared_copy in cui sono presenti l'incremento e il decremento atomico, ho sofferto di un problema di utilizzo della CPU molto più elevato. Non mi sarei mai aspettato che l'incremento e il decremento atomico potessero richiedere così tanti costi.

Dopo il risultato del test, l'incremento e il decremento atomici int32 impiegano 2 o 40 volte rispetto all'incremento e al decremento non atomici. L'ho preso su Core i7 3GHz con Windows 8.1. Il primo risultato viene fuori quando non si verifica alcuna contesa, il secondo quando si verifica un'alta possibilità di contesa. Tengo presente che le operazioni atomiche sono alla fine blocco basato su hardware. Lock is lock. Cattivo rendimento quando si verifica contesa.

Sperimentando questo, uso sempre byref (const shared_ptr &) che byval (shared_ptr).


1

C'è stato un recente post sul blog: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Quindi la risposta a questa domanda è: non (quasi) mai passare const shared_ptr<T>&.
Basta semplicemente passare la classe sottostante.

Fondamentalmente gli unici tipi di parametri ragionevoli sono:

  • shared_ptr<T> - Modifica e diventa proprietario
  • shared_ptr<const T> - Non modificare, diventa proprietario
  • T& - Modifica, nessuna proprietà
  • const T& - Non modificare, nessuna proprietà
  • T - Non modificare, nessuna proprietà, economico da copiare

Come sottolineato da @accel in https://stackoverflow.com/a/26197326/1930508, il consiglio di Herb Sutter è:

Usa const const_ptr & come parametro solo se non sei sicuro di prendere o meno una copia e condividere la proprietà

Ma in quanti casi non sei sicuro? Quindi questa è una situazione rara


0

È noto il problema che il passaggio di shared_ptr in base al valore ha un costo e, se possibile, dovrebbe essere evitato.

Il costo del passaggio di shared_ptr

La maggior parte del tempo che passa shared_ptr per riferimento, e ancora meglio per riferimento const, lo farebbe.

La linea guida di base cpp ha una regola specifica per il passaggio di shared_ptr

R.34: Prendi un parametro shared_ptr per esprimere che una funzione è il proprietario della parte

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Un esempio di quando il passaggio di shared_ptr in base al valore è veramente necessario è quando il chiamante passa un oggetto condiviso a un chiamante asincrono, ovvero il chiamante esce dall'ambito prima che il chiamante completi il ​​suo lavoro. Il chiamato deve "prolungare" la durata dell'oggetto condiviso prendendo un valore share_ptr. In questo caso, passare un riferimento a shared_ptr non lo farà.

Lo stesso vale per il passaggio di un oggetto condiviso a un thread di lavoro.


-4

shared_ptr non è abbastanza grande, né il suo costruttore \ distruttore fa abbastanza lavoro perché ci sia abbastanza overhead dalla copia per occuparsi delle prestazioni pass by reference vs pass by copy.


15
L'hai misurato?
curioso

2
@stonemetal: che dire delle istruzioni atomiche durante la creazione di new shared_ptr?
Quarra,

È un tipo non POD, quindi nella maggior parte degli ABI anche passandolo "per valore" passa effettivamente un puntatore. Non è affatto la vera copia di byte. Come puoi vedere nell'output asm, il passaggio di un shared_ptr<int>valore richiede oltre 100 istruzioni x86 (comprese le costose lockistruzioni ed per aumentare / diminuire atomicamente il conteggio dei riferimenti). Passare per riferimento costante equivale a passare un puntatore a qualsiasi cosa (e in questo esempio sull'esploratore del compilatore Godbolt, l'ottimizzazione di coda-coda trasforma questo in un semplice jmp invece di una chiamata: godbolt.org/g/TazMBU ).
Peter Cordes,

TL: DR: questo è C ++ in cui i costruttori di copie possono fare molto più lavoro che copiare semplicemente i byte. Questa risposta è spazzatura totale.
Peter Cordes,

2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Ad esempio, i puntatori condivisi passati per valore vs passa per riferimento vede una differenza di tempo di esecuzione di circa il 33%. Se stai lavorando su un codice critico per le prestazioni, i puntatori nudi aumentano notevolmente le prestazioni. Quindi, passa sicuramente da const ref se ti ricordi, ma non è un grosso problema se non lo fai. È molto più importante non usare shared_ptr se non ne hai bisogno.
Stonemetal
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.