Lo scopo di shared_ptr
un'istanza distinta è garantire (per quanto possibile) che finché questo shared_ptr
è nell'ambito, l'oggetto a cui punta esisterà ancora, 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, utilizzando un riferimento a a shared_ptr
, disabiliti tale 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 sp->do_something()
non esploderà a causa di un puntatore nullo?
Tutto dipende da cosa c'è in quelle sezioni "..." del codice. E se chiamassi qualcosa durante il primo '...' che ha l'effetto collaterale (da qualche parte in un'altra parte del codice) di cancellare a shared_ptr
su quello stesso oggetto? E se fosse l'unico che rimane distinto shared_ptr
da quell'oggetto? Ciao ciao oggetto, proprio dove stai per provare a usarlo.
Quindi ci sono due modi per rispondere a questa domanda:
Esamina molto attentamente l'origine dell'intero programma finché non sei sicuro che l'oggetto non morirà durante il corpo della funzione.
Modificare il parametro in modo che sia un oggetto distinto anziché un riferimento.
Un consiglio generale che si applica qui: non preoccuparti di apportare modifiche rischiose al tuo codice per motivi di prestazioni finché non hai programmato il tuo prodotto in una situazione realistica in un profiler e hai misurato in modo definitivo che il cambiamento che vuoi fare farà un differenza significativa rispetto alle prestazioni.
Aggiornamento per il commentatore JQ
Ecco un esempio artificioso. È deliberatamente semplice, quindi l'errore sarà ovvio. In esempi reali, l'errore non è così ovvio 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 invece di utilizzare un std::string
che probabilmente viene copiato mentre viene passato in più posizioni, utilizziamo shared_ptr
a una stringa:
void send_message(std::shared_ptr<std::string> msg)
{
std::cout << (*msg.get()) << std::endl;
}
(Per questo esempio lo "inviamo" alla console).
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 in fase di invio, 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 modifichiamo la nostra funzione in base alle 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 a inviare, scartiamo il messaggio precedente corrente e quindi, 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, viene stampato Hi!
due volte.
Ora arriva il signor Maintainer, che guarda il codice e pensa: Ehi, quel parametro send_message
è a shared_ptr
:
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 tipicamente grande su qualche canale, 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).
Il signor Maintainer è sorpreso da questo, ma aggiunge un controllo difensivo send_message
nel tentativo di impedire che il problema si verifichi:
void send_message(const std::shared_ptr<std::string> &msg)
{
if (msg == 0)
return;
Ma ovviamente va ancora avanti e va in crash, perché msg
non è mai nullo quando send_message
viene chiamato.
Come ho detto, con tutto il codice così vicino in un banale esempio, è facile trovare l'errore. Ma nei programmi reali, con relazioni più complesse tra oggetti mutabili che mantengono puntatori l'uno all'altro, è facile commettere l'errore e difficile costruire i casi di test necessari per rilevare l'errore.
La soluzione facile, in cui si desidera che una funzione possa fare affidamento sul fatto che shared_ptr
continui a essere non nulla per tutto il tempo, è che la funzione allochi il proprio vero shared_ptr
, piuttosto che fare affidamento su un riferimento a un esistente shared_ptr
.
Lo svantaggio è che la copia di a shared_ptr
non è gratuita: anche le implementazioni "lock-free" devono utilizzare un'operazione interlock per rispettare le garanzie di threading. Quindi ci possono essere situazioni in cui un programma può essere notevolmente velocizzato cambiando un shared_ptr
in a shared_ptr &
. Ma non si tratta di una modifica che può essere apportata in sicurezza a tutti i programmi. Cambia il significato logico del programma.
Nota che si verificherebbe un bug simile se usassimo std::string
tutto invece di std::shared_ptr<std::string>
e invece di:
previous_message = 0;
per chiarire il messaggio, abbiamo detto:
previous_message.clear();
Quindi il sintomo sarebbe l'invio accidentale di un messaggio vuoto, invece di un comportamento indefinito. Il costo di una copia extra di una stringa molto grande può essere molto più significativo del costo della copia di a shared_ptr
, quindi il compromesso potrebbe essere diverso.