Domanda

Se ho una funzione che deve funzionare con a shared_ptr, non sarebbe più efficiente passargli un riferimento (in modo da evitare di copiare il file shared_ptr oggetto)?Quali sono i possibili effetti collaterali negativi?Immagino due casi possibili:

1) all'interno della funzione viene fatta una copia dell'argomento, come in

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2) all'interno della funzione viene utilizzato solo l'argomento, come in

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

Non vedo in entrambi i casi un buon motivo per superare il test boost::shared_ptr<foo> per valore invece che per riferimento.Il passaggio per valore incrementerebbe solo "temporaneamente" il conteggio dei riferimenti a causa della copia, quindi lo decrementerebbe quando si esce dall'ambito della funzione.Sto trascurando qualcosa?

Giusto per chiarire, dopo aver letto diverse risposte:Sono perfettamente d'accordo sui problemi di ottimizzazione prematura e cerco sempre di prima profilare e poi lavorare sugli hotspot.La mia domanda riguardava più un punto di vista del codice puramente tecnico, se capisci cosa intendo.

È stato utile?

Soluzione

Il punto di un'istanza shared_ptr distinta è garantire (per quanto possibile) che fino a quando questo sp->do_something() è nell'ambito, l'oggetto a cui punta rimarrà, perché il suo conteggio di riferimento sarà almeno 1.

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
    // sp points to an object that cannot be destroyed during this function
}

Quindi, usando un riferimento a std::string, disabiliti quella garanzia. Quindi nel tuo secondo caso:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

Come fai a sapere che Hi! non esploderà a causa di un puntatore null?

Tutto dipende da cosa c'è in quelle sezioni '...' del codice. E se chiami qualcosa durante il primo '...' che ha l'effetto collaterale (da qualche parte in un'altra parte del codice) di cancellare un send_message allo stesso oggetto? E se fosse l'unica cosa rimasta distinta msg a quell'oggetto? Ciao ciao oggetto, proprio dove stai per provare a usarlo.

Quindi ci sono due modi per rispondere a questa domanda:

  1. Esamina attentamente la fonte dell'intero programma finché non sei sicuro che l'oggetto non morirà durante il corpo della funzione.

  2. Riporta il parametro in modo che diventi un oggetto distinto anziché un riferimento.

Un piccolo consiglio generale che si applica qui: non preoccuparti di apportare modifiche rischiose al tuo codice per motivi di prestazioni fino a quando non hai cronometrato il tuo prodotto in una situazione realistica in un profiler e misurato definitivamente che la modifica che desideri apportare farà una differenza significativa nelle prestazioni.

Aggiornamento per commentatore JQ

Ecco un esempio inventato. È volutamente semplice, quindi l'errore sarà evidente. In esempi reali, l'errore non è così evidente perché è nascosto in strati di dettagli reali.

Abbiamo una funzione che invierà un messaggio da qualche parte. Potrebbe essere un messaggio di grandi dimensioni, quindi piuttosto che utilizzare un shared_ptr & che probabilmente viene copiato quando viene passato in più punti, usiamo un std::shared_ptr<std::string> in una stringa:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(Abbiamo appena " invia " esso alla console per questo esempio).

Ora vogliamo aggiungere una funzione per ricordare il messaggio precedente. Vogliamo il seguente comportamento: deve esistere una variabile che contiene il messaggio inviato più di recente, ma mentre un messaggio è attualmente inviato, non deve esserci alcun messaggio precedente (la variabile deve essere ripristinata prima dell'invio). Quindi dichiariamo la nuova variabile:

std::shared_ptr<std::string> previous_message;

Quindi modificiamo la nostra funzione secondo le regole che abbiamo specificato:

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

Quindi, prima di iniziare l'invio, scartiamo il messaggio precedente corrente, e dopo che l'invio è completo possiamo memorizzare il nuovo messaggio precedente. Tutto bene. Ecco un po 'di codice di prova:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

E come previsto, questo stampa <=> due volte.

Ora arriva Mr Maintainer, che guarda il codice e pensa: Hey, quel parametro su <=> è un <=>:

void send_message(std::shared_ptr<std::string> msg)

Ovviamente può essere modificato in:

void send_message(const std::shared_ptr<std::string> &msg)

Pensa al miglioramento delle prestazioni che questo porterà! (Non importa che stiamo per inviare un messaggio in genere di grandi dimensioni su alcuni canali, quindi il miglioramento delle prestazioni sarà così piccolo da non essere misurabile).

Ma il vero problema è che ora il codice di test mostrerà un comportamento indefinito (nelle build di debug di Visual C ++ 2010, si arresta in modo anomalo).

Mr Maintainer ne è sorpreso, ma aggiunge un controllo difensivo a <=> nel tentativo di evitare che si verifichi il problema:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

Ma ovviamente continua e va in crash, perché <=> non è mai nullo quando viene chiamato <=>.

Come ho detto, con tutto il codice così ravvicinato in un esempio banale, è facile trovare l'errore. Ma nei programmi reali, con relazioni più complesse tra oggetti mutabili che tengono puntati l'un l'altro, è facile commettere l'errore e difficile costruire i casi di test necessari per rilevare l'errore.

La soluzione semplice, in cui si desidera che una funzione sia in grado di fare affidamento su un <=> continuando a non essere nulla, è che la funzione alloca il proprio vero <=>, piuttosto che fare affidamento su un riferimento a un <=>.

esistente

Il rovescio della medaglia è che la copia di <=> non è gratuita: anche " lock-free & Quot; le implementazioni devono utilizzare un'operazione interbloccata per onorare le garanzie di threading. Quindi potrebbero esserci situazioni in cui un programma può essere notevolmente accelerato cambiando un <=> in un <=>. Ma questo non è un cambiamento che può essere apportato in modo sicuro a tutti i programmi. Cambia il significato logico del programma.

Si noti che si verificherebbe un bug simile se si utilizzasse <=> in tutto anziché <=> e invece di:

previous_message = 0;

per cancellare il messaggio, abbiamo detto:

previous_message.clear();

Quindi il sintomo sarebbe l'invio accidentale di un messaggio vuoto, anziché un comportamento indefinito. Il costo di una copia aggiuntiva di una stringa molto grande può essere molto più significativo del costo della copia di un <=>, quindi il compromesso potrebbe essere diverso.

Altri suggerimenti

Mi sono trovato in disaccordo con la risposta più votata, quindi sono andato alla ricerca di pareri di esperti ed eccoli qui. Da http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: " quando si passa a shared_ptr, le copie costano "

Scott Meyers: " Non c'è niente di speciale in shared_ptr quando si tratta di passarlo per valore o passarlo per riferimento. Usa esattamente la stessa analisi che usi per qualsiasi altro tipo definito dall'utente. Le persone sembrano avere questa percezione che shared_ptr risolva in qualche modo tutti i problemi di gestione e che, poiché è piccolo, è necessariamente poco costoso passare per valore. Deve essere copiato, e c'è un costo associato a quello ... è costoso passarlo per valore, quindi se riesco a cavarmela con la semantica corretta nel mio programma, lo passerò facendo riferimento a const oppure fai riferimento invece a "

Herb Sutter: " passali sempre per riferimento a const, e molto occasionalmente forse perché sai che cosa hai chiamato potrebbe modificare la cosa da cui hai ottenuto un riferimento, forse potresti passare per valore ... se li copi come parametri, oh mio Dio non hai quasi mai bisogno di aumentare quel conteggio di riferimento perché è comunque tenuto in vita e dovresti passarlo per riferimento, quindi per favore "

Aggiornamento: Herb si è espanso su questo qui: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , anche se la morale della storia è che non dovresti passare affatto a shared_ptr quot; a meno che non si desideri utilizzare o manipolare il puntatore intelligente stesso, ad esempio per condividere o trasferire la proprietà. "

Vorrei sconsigliare questa pratica a meno che tu e gli altri programmatori con cui lavori davvero, davvero sappiate cosa state facendo tutti.

Innanzitutto, non hai idea di come potrebbe evolversi l'interfaccia della tua classe e vuoi impedire ad altri programmatori di fare cose cattive. Passare un shared_ptr per riferimento non è qualcosa che un programmatore dovrebbe aspettarsi di vedere, perché non è idiomatico e ciò rende facile usarlo in modo errato. Programma difensivo: rendere l'interfaccia difficile da usare in modo errato. Il passaggio per riferimento invierà problemi in seguito.

In secondo luogo, non ottimizzare fino a quando non sai che questa particolare classe sarà un problema. Profilo prima, e poi se il tuo programma ha davvero bisogno della spinta data passando per riferimento, allora forse. Altrimenti, non sudare le piccole cose (cioè le istruzioni N aggiuntive che ci vogliono per passare per valore) invece preoccupati di design, strutture di dati, algoritmi e manutenibilità a lungo termine.

Sì, prendere un riferimento va bene lì.Non intendi conferire al metodo la proprietà condivisa;vuole solo lavorare con esso.Potresti prendere un riferimento anche per il primo caso, visto che lo copi comunque.Ma per il primo caso, esso prende proprietà.C'è questo trucco per copiarlo ancora solo una volta:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

Dovresti copiarlo anche quando lo restituisci (ovvero non restituire un riferimento).Perché la tua classe non sa cosa sta facendo il client con esso (potrebbe memorizzare un puntatore ad esso e poi avviene il big bang).Se in seguito si scopre che si tratta di un collo di bottiglia (primo profilo!), è comunque possibile restituire un riferimento.


Modificare:Naturalmente, come altri sottolineano, questo è vero solo se conosci il tuo codice e sai che non reimposti in qualche modo il puntatore condiviso passato.In caso di dubbio, passa semplicemente per valore.

È ragionevole passare shared_ptr s di const&. Probabilmente non causerà problemi (tranne nel caso improbabile che il riferimento boost::shared_ptr venga eliminato durante la chiamata di funzione, come dettagliato da Earwicker) e sarà probabilmente più veloce se ne passerai molti in giro. Ricorda; l'impostazione predefinita & è thread-safe, quindi la copia include un incremento thread-safe.

Prova a usare <=> anziché solo <=>, perché gli oggetti temporanei potrebbero non essere passati per riferimento non const. (Anche se un'estensione di lingua in MSVC ti consente di farlo comunque)

Nel secondo caso, farlo è più semplice:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

Puoi chiamarlo come

only_work_with_sp(*sp);

Eviterei un " plain " riferimento a meno che la funzione non possa esplicitamente modificare il puntatore.

Un const & può essere una microottimizzazione ragionevole quando si chiamano piccole funzioni - ad es. per consentire ulteriori ottimizzazioni, come la definizione di alcune condizioni. Inoltre, l'incremento / decremento, poiché è thread-safe, è un punto di sincronizzazione. Tuttavia, non mi aspetto che questo faccia una grande differenza nella maggior parte degli scenari.

Generalmente, dovresti usare lo stile più semplice a meno che tu non abbia motivo di non farlo. Quindi, usa <=> in modo coerente o aggiungi un commento sul perché se lo usi solo in alcuni punti.

Vorrei sostenere il passaggio del puntatore condiviso per riferimento const - una semantica che la funzione che viene passata con il puntatore NON possiede il puntatore, che è un linguaggio pulito per gli sviluppatori.

L'unico inconveniente è nei programmi con più thread l'oggetto che viene puntato dal puntatore condiviso viene distrutto in un altro thread. Quindi è sicuro di dire che usare il riferimento const del puntatore condiviso è sicuro in un programma a thread singolo.

Il passaggio del puntatore condiviso per riferimento non const è talvolta pericoloso - il motivo è le funzioni di scambio e ripristino che la funzione può invocare al fine di distruggere l'oggetto che è ancora considerato valido dopo il ritorno della funzione.

Immagino non si tratti di ottimizzazione prematura - si tratta di evitare inutili sprechi di cicli della CPU quando si è chiari su ciò che si desidera fare e il linguaggio di programmazione è stato fermamente adottato dai colleghi sviluppatori.

Solo i miei 2 centesimi :-)

Sembra che tutti i pro e contro qui possano effettivamente essere generalizzati a QUALSIASI tipo passato per riferimento, non solo shared_ptr. A mio avviso, dovresti conoscere la semantica del passaggio per riferimento, riferimento costante e valore e usarlo correttamente. Ma non c'è assolutamente nulla di intrinsecamente sbagliato nel passare shared_ptr per riferimento, a meno che tu non pensi che tutti i riferimenti siano cattivi ...

Per tornare all'esempio:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

Come fai a sapere che sp.do_something() non esploderà a causa di un puntatore penzolante?

La verità è che, shared_ptr o no, const o no, questo potrebbe accadere se si dispone di un difetto di progettazione, come la condivisione diretta o indiretta della proprietà di sp tra thread, la mancanza di un oggetto che delete this hanno una proprietà circolare o altri errori di proprietà.

Una cosa che non ho ancora visto menzionato è che quando si passano i puntatori condivisi per riferimento, si perde la conversione implicita che si ottiene se si desidera passare un puntatore condiviso della classe derivata attraverso un riferimento a un puntatore condiviso della classe base .

Ad esempio, questo codice produrrà un errore, ma funzionerà se si modifica test() in modo che il puntatore condiviso non venga passato per riferimento.

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}

Presumo che tu abbia familiarità con ottimizzazione prematura e lo stai chiedendo per scopi accademici o perché hai isolato del codice preesistente che sta funzionando male.

Il passaggio per riferimento va bene

Il passaggio per riferimento const è migliore e di solito può essere utilizzato, in quanto non forza la costanza sull'oggetto indicato.

Sei non a rischio di perdere il puntatore a causa dell'utilizzo di un riferimento. Tale riferimento è la prova che hai una copia del puntatore intelligente in precedenza nello stack e che solo un thread possiede uno stack di chiamate, in modo che la copia preesistente non scompaia.

L'uso dei riferimenti è spesso più efficiente per i motivi menzionati, ma non garantito . Ricorda che il dereferenziamento di un oggetto può richiedere del lavoro. Il tuo scenario di utilizzo di riferimento ideale sarebbe se il tuo stile di codifica comporta molte piccole funzioni, in cui il puntatore viene passato da una funzione all'altra prima di essere utilizzato.

Dovresti evitare sempre di memorizzare il tuo puntatore intelligente come riferimento. Il tuo Class::take_copy_of_sp(&sp) esempio mostra un uso corretto per questo.

Supponendo che non ci occupiamo della correttezza const (o più, intendi consentire alle funzioni di essere in grado di modificare o condividere la proprietà dei dati trasmessi), passare un boost :: shared_ptr in base al valore è più sicuro che passarlo per riferimento, poiché consentiamo al boost originale :: shared_ptr di controllare la propria vita. Considera i risultati del seguente codice ...

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

Dall'esempio sopra vediamo che passare sharedA per riferimento consente a FooTakesReference di ripristinare il puntatore originale, il che riduce il conteggio degli usi a 0, distruggendo i suoi dati. FooTakesValue , tuttavia, non è in grado di ripristinare il puntatore originale, garantendo che i dati di sharedB siano ancora utilizzabili. Quando inevitabilmente arriva un altro sviluppatore e tenta di tornare sulle spalle dell'esistenza fragile di sharedA , ne consegue il caos. Il fortunato sviluppatore sharedB , tuttavia, torna a casa presto perché nel suo mondo va tutto bene.

La sicurezza del codice, in questo caso, supera di gran lunga qualsiasi creazione di miglioramento della velocità. Allo stesso tempo, boost :: shared_ptr ha lo scopo di migliorare la sicurezza del codice. Sarà molto più semplice passare da una copia a un riferimento, se qualcosa richiede questo tipo di ottimizzazione di nicchia.

Sandy ha scritto: " Sembra che tutti i pro e contro qui possano effettivamente essere generalizzati a QUALSIASI tipo passato per riferimento, non solo shared_ptr. "

Vero in una certa misura, ma il punto di usare shared_ptr è quello di eliminare le preoccupazioni relative alla durata degli oggetti e lasciare che il compilatore gestisca ciò per te. Se si intende passare un puntatore condiviso per riferimento e consentire ai client dei metodi non const di chiamare oggetti conteggiati riferimento che potrebbero liberare i dati dell'oggetto, l'utilizzo di un puntatore condiviso è quasi inutile.

Ho scritto " quasi " in quella frase precedente perché le prestazioni possono essere un problema, e "potrebbe" essere giustificato in rari casi, ma vorrei anche evitare questo scenario e cercare personalmente tutte le altre possibili soluzioni di ottimizzazione, come cercare di aggiungere seriamente un altro livello di indiretta, valutazione pigra, ecc.

Il codice che esiste oltre il suo autore, o addirittura pubblica la sua memoria dell'autore, che richiede ipotesi implicite sul comportamento, in particolare sul comportamento della vita degli oggetti, richiede una documentazione chiara, concisa e leggibile, e quindi molti client non la leggeranno comunque! La semplicità supera quasi sempre l'efficienza e ci sono quasi sempre altri modi per essere efficienti. Se hai davvero bisogno di passare valori per riferimento per evitare una copia approfondita da parte dei costruttori di copie dei tuoi oggetti contati con riferimento (e l'operatore uguale), allora forse dovresti considerare i modi per rendere i dati copiati in profondità come puntatori contati di riferimento che possono essere copiato rapidamente. (Naturalmente, questo è solo uno scenario di progettazione che potrebbe non essere applicabile alla tua situazione).

Lavoravo in un progetto secondo il quale il principio era molto forte nel passaggio di indicatori intelligenti per valore. Quando mi è stato chiesto di fare alcune analisi delle prestazioni - ho scoperto che per l'incremento e il decremento dei contatori di riferimento dei puntatori intelligenti l'applicazione trascorre tra il 4-6% del tempo del processore utilizzato.

Se vuoi passare i suggerimenti intelligenti in base al valore solo per evitare problemi in casi strani come descritto da Daniel Earwicker, assicurati di capire il prezzo che stai pagando.

Se si decide di utilizzare un riferimento, il motivo principale per utilizzare il riferimento const è quello di rendere possibile l'upcasting implicito quando è necessario passare il puntatore condiviso all'oggetto dalla classe che eredita la classe utilizzata nell'interfaccia.

Oltre a ciò che ha detto litb, vorrei sottolineare che probabilmente passerà per riferimento const nel secondo esempio, in questo modo sei sicuro di non modificarlo accidentalmente.

struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. passa per valore:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
    
  2. passa per riferimento:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
    
  3. passa per puntatore:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }
    

Ogni pezzo di codice deve avere un senso. Se passi un puntatore condiviso per valore ovunque nell'applicazione, questo significa & Quot; Non sono sicuro di cosa stia succedendo altrove, quindi preferisco la sicurezza non elaborata quot ;. Questo non è ciò che chiamo segno di buona fiducia per altri programmatori che potrebbero consultare il codice.

Ad ogni modo, anche se una funzione ottiene un riferimento const e tu sei " unsure " ;, puoi comunque creare una copia del puntatore condiviso all'inizio della funzione, per aggiungere un riferimento forte al puntatore. Questo potrebbe anche essere visto come un suggerimento per il design (& Quot; il puntatore potrebbe essere modificato altrove & Quot;).

Quindi sì, IMO, il valore predefinito dovrebbe essere " passa per riferimento const " ;.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top