Passaggio di puntatori condivisi come argomenti


89

Se dichiaro un oggetto racchiuso in un puntatore condiviso:

std::shared_ptr<myClass> myClassObject(new myClass());

quindi ho voluto passarlo come argomento a un metodo:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

Quanto sopra incrementa semplicemente il conteggio dei riferimenti di shared_pt e tutto va bene? O lascia un puntatore penzolante?

Dovresti ancora farlo ?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Penso che il secondo modo possa essere più efficiente perché deve copiare solo 1 indirizzo (al contrario dell'intero puntatore intelligente), ma il primo modo sembra più leggibile e non prevedo di spingere i limiti delle prestazioni. Voglio solo assicurarmi che non ci sia qualcosa di pericoloso in questo.

Grazie.


14
const std::shared_ptr<myClass>& arg1
Capitano Obvlious

3
Il secondo modo è rotto, il primo è idiomatico se hai effettivamente bisogno della tua funzione per condividere la proprietà. Ma è DoSomethingdavvero necessario condividere la proprietà? Sembra che dovrebbe prendere solo un riferimento invece ...
ildjarn

8
@SteveH: Non è così, ma perché la tua funzione dovrebbe forzare la semantica di proprietà dell'oggetto speciale sui suoi chiamanti se non ne ha effettivamente bisogno? La tua funzione non fa nulla che non sarebbe migliore come void DoSomething(myClass& arg1).
ildjarn

2
@SteveH: L'intero scopo dei puntatori intelligenti è gestire i problemi ordinari di proprietà degli oggetti - se non li hai, non dovresti usare i puntatori intelligenti in primo luogo. ; -] Per quanto riguarda shared_ptr<>specificamente, è necessario passare per valore al fine di condividere effettivamente la proprietà.
ildjarn

4
Non correlato: in generale, std::make_sharednon è solo più efficiente, ma anche più sicuro del std::shared_ptrcostruttore.
R. Martinho Fernandes

Risposte:


171

Voglio passare un puntatore condiviso a una funzione. Puoi aiutarmi con quello?

Certo, posso aiutarti con te. Presumo che tu abbia una certa comprensione della semantica della proprietà in C ++. È vero?

Sì, sono abbastanza a mio agio con l'argomento.

Buona.

Ok, posso solo pensare a due motivi per sostenere un shared_ptrargomento:

  1. La funzione vuole condividere la proprietà dell'oggetto;
  2. La funzione esegue alcune operazioni che funzionano specificamente su shared_ptrs.

Quale ti interessa?

Sto cercando una risposta generale, quindi in realtà sono interessato a entrambi. Sono curioso di sapere cosa intendi nel caso n. 2, però.

Esempi di tali funzioni includono std::static_pointer_castcomparatori personalizzati o predicati. Ad esempio, se è necessario trovare tutti i shared_ptr univoci da un vettore, è necessario un tale predicato.

Ah, quando la funzione ha effettivamente bisogno di manipolare il puntatore intelligente stesso.

Esattamente.

In tal caso, penso che dovremmo passare per riferimento.

Sì. E se non cambia il puntatore, vuoi passare per riferimento const. Non è necessario copiare poiché non è necessario condividere la proprietà. Questo è l'altro scenario.

Ok capito. Parliamo dell'altro scenario.

Quello di cui condividi la proprietà? Ok. Come condividi la proprietà con shared_ptr?

Copiandolo.

Quindi la funzione dovrà fare una copia di un shared_ptr, corretto?

Ovviamente. Quindi lo passo con un riferimento a const e copio in una variabile locale?

No, è una pessimazione. Se viene passato per riferimento, la funzione non avrà altra scelta che eseguire la copia manualmente. Se viene passato per valore, il compilatore sceglierà la scelta migliore tra una copia e una mossa e la eseguirà automaticamente. Quindi, passa per valore.

Buon punto. Devo ricordare più spesso l'articolo " Vuoi velocità? Passa per valore ".

Aspetta, cosa succede se la funzione memorizza il shared_ptrin una variabile membro, per esempio? Non sarà una copia ridondante?

La funzione può semplicemente spostare l' shared_ptrargomento nella sua memoria. Spostare un shared_ptrè economico perché non modifica alcun conteggio dei riferimenti.

Ah, buona idea.

Ma sto pensando a un terzo scenario: cosa succede se non si desidera manipolare shared_ptrné condividere la proprietà?

In tal caso, shared_ptrè completamente irrilevante per la funzione. Se vuoi manipolare le punte, prendi una punta e lascia che i chiamanti scelgano quale semantica di proprietà desiderano.

E dovrei prendere le punte per riferimento o per valore?

Si applicano le solite regole. I puntatori intelligenti non cambiano nulla.

Passa per valore se intendo copiare, passa per riferimento se voglio evitare una copia.

Destra.

Hmm. Penso che tu abbia dimenticato l'ennesimo scenario. E se volessi condividere la proprietà, ma solo in base a una determinata condizione?

Ah, un interessante caso limite. Non mi aspetto che succeda spesso. Ma quando succede puoi passare per valore e ignorare la copia se non ne hai bisogno, oppure passare per riferimento e fare la copia se ne hai bisogno.

Rischio una copia ridondante nella prima opzione e perdo una potenziale mossa nella seconda. Non posso mangiare la torta e averla anche io?

Se ti trovi in ​​una situazione in cui questo è davvero importante, puoi fornire due overload, uno che prende un riferimento const lvalue e un altro che prende un riferimento rvalue. Uno copia, l'altro si muove. Un modello di funzione di inoltro perfetto è un'altra opzione.

Penso che copra tutti i possibili scenari. Grazie mille.


2
@ Jon: per cosa? Questo è tutto devo fare A o devo fare B . Penso che tutti sappiamo come passare gli oggetti per valore / riferimento, no?
sbi

9
Perché una prosa come "Significa che la passerò con un riferimento a const per fare la copia? No, questa è una pessimazione" confonde un principiante. Il passaggio di un riferimento a const non ne crea una copia.
Jon

1
@Martinho Non sono d'accordo con la regola "Se vuoi manipolare le punte, prendi una punta". Come affermato in questa risposta, non assumere la proprietà temporanea di un oggetto può essere pericoloso. A mio parere l'impostazione predefinita dovrebbe essere quella di passare una copia per la proprietà temporanea per garantire la validità dell'oggetto per la durata della chiamata di funzione.
radman

@radman Nessuno scenario nella risposta a cui ti colleghi applica quella regola, quindi è completamente irrilevante. Per favore scrivimi un SSCCE dove passi per riferimento da a shared_ptr<T> ptr;(cioè void f(T&)chiamato con f(*ptr)) e l'oggetto non sopravvive alla chiamata. In alternativa scrivine uno in cui passi per valore void f(T)e per problemi.
R. Martinho Fernandes

@ Martinho controlla il mio codice di esempio per il semplice esempio corretto autonomo. L'esempio è per void f(T&)e coinvolge i thread. Penso che il mio problema sia che il tono della tua risposta scoraggia il passaggio di proprietà di shared_ptr, che è il modo più sicuro, intuitivo e meno complicato di usarli. Sarei estremamente contrario a consigliare a un principiante di estrarre un riferimento a dati di proprietà di un shared_ptr <> le possibilità di uso improprio sono grandi e il vantaggio è minimo.
radman

22

Penso che le persone abbiano inutilmente paura di usare puntatori grezzi come parametri di funzione. Se la funzione non memorizza il puntatore o influisce in altro modo sulla sua durata, un puntatore non elaborato funziona altrettanto bene e rappresenta il minimo comune denominatore. Considera ad esempio come passeresti a unique_ptrin una funzione che accetta a shared_ptrcome parametro, per valore o per riferimento const?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Un puntatore non elaborato come parametro di funzione non impedisce di utilizzare puntatori intelligenti nel codice chiamante, dove è davvero importante.


8
Se stai usando un puntatore, perché non usare solo un riferimento? DoSomething(*a_smart_ptr)
Xeo

5
@Xeo, hai ragione, sarebbe ancora meglio. A volte è necessario consentire la possibilità di un puntatore NULL.
Mark Ransom

1
Se si ha la convenzione di usare puntatori non elaborati come parametri di output, è più facile individuare nel codice di chiamata che una variabile può cambiare, ad esempio: confronta fun(&x)con fun(x). In quest'ultimo esempio xpotrebbe essere passato per valore o come const ref. Come detto sopra ti permetterà anche di passare nullptrse non sei interessato all'uscita ...
Andreas Magnusson

2
@AndreasMagnusson: quella convenzione del parametro del puntatore come output può dare un falso senso di sicurezza quando si tratta di puntatori passati da un'altra fonte. Perché in quel caso la chiamata è divertente (x) e * x viene modificata e la fonte non ha & x per avvisarti.
Zan Lynx

cosa succede se la chiamata di funzione sopravvive all'ambito del chiamante? Esiste la possibilità che il distruttore del ptr condiviso sia stato chiamato e ora la funzione tenterà di accedere alla memoria cancellata, risultando in UB
Sridhar Thiagarajan

4

Sì, l'intera idea di shared_ptr <> è che più istanze possono contenere lo stesso puntatore grezzo e la memoria sottostante verrà liberata solo quando l'ultima istanza di shared_ptr <> viene distrutta.

Eviterei un puntatore a shared_ptr <> in quanto ciò vanifica lo scopo poiché ora hai di nuovo a che fare con raw_pointer.


2

Passare per valore nel tuo primo esempio è sicuro, ma c'è un modo di dire migliore. Passa per riferimento const quando possibile: direi di sì anche quando si tratta di puntatori intelligenti. Il tuo secondo esempio non è esattamente rotto ma è molto !???. Sciocco, non realizzare nulla e sconfigge parte del punto di puntatori intelligenti, e ti lascerà in un mondo pieno di buggy quando cerchi di dereferenziare e modificare le cose.


3
Nessuno sta sostenendo il passaggio per puntatore, se intendi puntatore grezzo. Passare per riferimento se non ci sono contese di proprietà è certamente idiomatico. Il punto è, nello shared_ptr<>specifico, che devi farne una copia per condividere effettivamente la proprietà; se hai un riferimento o un puntatore a un, shared_ptr<>non stai condividendo nulla e sei soggetto agli stessi problemi di durata di un normale riferimento o puntatore.
ildjarn

2
@ildjarn: in questo caso va bene passare un riferimento costante. L'originale shared_ptrdel chiamante è garantito per durare più a lungo della chiamata di funzione, quindi la funzione è sicura nell'usare l'estensione shared_ptr<> const &. Se ha bisogno di memorizzare il puntatore per un secondo momento, dovrà copiare (a questo punto deve essere fatta una copia, piuttosto che contenere un riferimento), ma non è necessario sostenere il costo della copia a meno che non sia necessario farlo esso. Vorrei passare un riferimento costante in questo caso ....
David Rodríguez - dribeas

3
@David: "Il passaggio di un riferimento costante va bene in questo caso. " Non in nessuna API sana - perché un'API dovrebbe imporre un tipo di puntatore intelligente di cui non si avvale nemmeno piuttosto che prendere un normale riferimento const? È grave quasi quanto la mancanza di correttezza const. Se il tuo punto è che tecnicamente non sta danneggiando nulla, allora sono certamente d'accordo, ma non penso che sia la cosa giusta da fare.
ildjarn

2
... se d'altra parte, la funzione non ha bisogno di estendere la durata dell'oggetto, allora puoi semplicemente rimuovere shared_ptrdall'interfaccia e passare un constriferimento semplice ( ) all'oggetto puntato.
David Rodríguez - dribeas

3
@David: cosa succede se il tuo consumatore API non sta usando shared_ptr<>per cominciare? Ora la tua API è davvero solo un rompicoglioni, poiché costringe il chiamante a cambiare inutilmente la semantica della durata dell'oggetto solo per usarla. Non vedo nulla che valga la pena sostenere in questo, oltre a dire "tecnicamente non fa male a nulla". Non vedo nulla di controverso su "se non hai bisogno della proprietà condivisa, non utilizzare shared_ptr<>".
ildjarn

0

nella tua funzione DoSomething stai cambiando un membro dati di un'istanza di classe myClass quindi quello che stai modificando è l'oggetto gestito (puntatore grezzo) non l'oggetto (shared_ptr). Ciò significa che al punto di ritorno di questa funzione tutti i puntatori condivisi al puntatore raw gestito vedranno il loro membro dati: myClass::someFieldmodificato in un valore diverso.

in questo caso, stai passando un oggetto a una funzione con la garanzia di non modificarlo (parlando di shared_ptr non dell'oggetto posseduto).

L'idioma per esprimere questo è attraverso: a const ref, in questo modo

void DoSomething(const std::shared_ptr<myClass>& arg)

Allo stesso modo stai assicurando all'utente della tua funzione che non stai aggiungendo un altro proprietario all'elenco dei proprietari del puntatore grezzo. Tuttavia, hai mantenuto la possibilità di modificare l'oggetto sottostante puntato dal puntatore non elaborato.

CAVEAT: Il che significa che, se in qualche modo qualcuno chiama shared_ptr::resetprima di chiamare la tua funzione, e in quel momento è l'ultimo shared_ptr a possedere raw_ptr, allora il tuo oggetto verrà distrutto e la tua funzione manipolerà un Pointer penzolante per l'oggetto distrutto. MOLTO PERICOLOSO!!!

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.