std :: shared_ptr come ultima risorsa?

58

Stavo solo guardando i flussi di "Going Native 2012" e ho notato la discussione su std::shared_ptr . Sono stato un po 'sorpreso di sentire la visione alquanto negativa di Bjarne su std::shared_ptr e il suo commento che dovrebbe essere usato come "ultima risorsa" quando la vita di un oggetto è incerta (che credo, secondo lui, non dovrebbe essere di caso).

Qualcuno dovrebbe importare di spiegarlo in modo più approfondito? Come possiamo programmare senza std::shared_ptr e gestire ancora i tempi di vita degli oggetti in modo sicuro ?

    
posta ronag 04.02.2012 - 15:47
fonte

9 risposte

56

Se puoi evitare la proprietà condivisa, la tua applicazione sarà più semplice e comprensibile e quindi meno suscettibile ai bug introdotti durante la manutenzione. Modelli di proprietà complessi o poco chiari tendono a portare ad accoppiamenti difficili da seguire di diverse parti dell'applicazione attraverso uno stato condiviso che potrebbe non essere facilmente rintracciabile.

Dato questo, è preferibile usare oggetti con durata di archiviazione automatica e avere sotto-oggetti "valore". In caso contrario, unique_ptr potrebbe essere una buona alternativa con shared_ptr che è - se non l'ultima risorsa - in qualche modo in basso nella lista degli strumenti desiderabili.

    
risposta data 04.02.2012 - 16:00
fonte
47

Il mondo in cui vive Bjarne è molto ... accademico, per mancanza di un termine migliore. Se il tuo codice può essere progettato e strutturato in modo tale che gli oggetti abbiano gerarchie relazionali molto deliberate, in modo tale che le relazioni di proprietà siano rigide e inflessibili, il codice scorre in una direzione (da alto a basso) e gli oggetti parlano solo a quelli più bassi la gerarchia, quindi non troverai molto bisogno di shared_ptr . È qualcosa che usi in quelle rare occasioni in cui qualcuno deve infrangere le regole. In caso contrario, puoi semplicemente inserire tutto in vector s o altre strutture dati che utilizzano la semantica del valore e unique_ptr s per le cose che devi allocare singolarmente.

Mentre quello è un grande mondo in cui vivere, non è quello che si fa a fare tutto il tempo. Se non puoi organizzare il tuo codice in quel modo, perché il design del sistema che stai cercando di rendere significa che è impossibile (o semplicemente spiacevole), allora ti troverai proprietà condivisa di oggetti sempre di più.

In un sistema del genere, tenere puntatori nudi non ... è pericoloso esattamente, ma solleva domande. Il bello di shared_ptr è che fornisce garanzie ematiche ragionevoli sulla durata dell'oggetto. Può essere rotto? Ovviamente. Ma le persone possono anche const_cast di cose; l'assistenza di base e l'alimentazione di shared_ptr dovrebbero fornire una ragionevole qualità di vita per gli oggetti allocati a cui deve essere condivisa la proprietà.

Quindi, ci sono weak_ptr s, che non possono essere utilizzati in assenza di shared_ptr . Se il tuo sistema è rigidamente strutturato, puoi memorizzare un puntatore nudo su un oggetto, sicuro sapendo che la struttura dell'applicazione garantisce che l'oggetto puntato sopravviverà a te. È possibile chiamare una funzione che restituisce un puntatore a un valore interno o esterno (trovare l'oggetto denominato X, ad esempio). Nel codice correttamente strutturato, tale funzione sarebbe disponibile solo se la durata dell'oggetto fosse garantita per superare il proprio; quindi, conservare quel puntatore nudo nel tuo oggetto va bene.

Poiché questa rigidità non è sempre possibile ottenere nei sistemi reali, è necessario un modo per garantire ragionevolmente la durata. A volte, non hai bisogno di piena proprietà; a volte, devi solo essere in grado di sapere quando il puntatore è cattivo o buono. Ecco dove arriva weak_ptr . Ci sono stati casi in cui I potrebbe aver usato un unique_ptr o boost::scoped_ptr , ma ho dovuto usare un shared_ptr perché I specificamente necessario per dare a qualcuno un puntatore "volatile". Un puntatore che è a vita era indeterminato e potevano interrogare quando quel puntatore veniva distrutto.

Un modo sicuro per sopravvivere quando lo stato del mondo è indeterminato.

Potrebbe essere stato fatto da qualche chiamata di funzione per ottenere il puntatore, invece che tramite weak_ptr ? Sì, ma potrebbe essere più facilmente infranto. Una funzione che restituisce un puntatore nudo non ha modo di suggerire sintatticamente che l'utente non faccia qualcosa come memorizzare quel puntatore a lungo termine. Restituire un shared_ptr rende inoltre troppo facile per qualcuno semplicemente memorizzarlo e potenzialmente prolungare la durata di vita di un oggetto. Restituire un weak_ptr tuttavia suggerisce strongmente che l'archiviazione del shared_ptr ottenuto da lock è un'idea ... dubbiosa. Non ti impedirà di farlo, ma nulla in C ++ ti impedisce di rompere il codice. weak_ptr fornisce una minima resistenza dal fare la cosa naturale.

Ora, questo non vuol dire che shared_ptr non possa essere sovrautilizzato ; certamente può Soprattutto pre- unique_ptr , ci sono stati molti casi in cui ho appena usato un boost::shared_ptr perché avevo bisogno di passare un puntatore RAII o di inserirlo in un elenco. Senza spostare la semantica e unique_ptr , boost::shared_ptr era l'unica soluzione reale.

E puoi usarlo in posti in cui non è necessario. Come detto sopra, la corretta struttura del codice può eliminare la necessità di alcuni usi di shared_ptr . Ma se il tuo sistema non può essere strutturato come tale e continua a fare ciò di cui ha bisogno, shared_ptr sarà di grande utilità.

    
risposta data 04.02.2012 - 17:11
fonte
37

Non credo di aver mai usato std::shared_ptr .

La maggior parte delle volte, un oggetto è associato ad una collezione, a cui appartiene per tutta la sua durata. In questo caso puoi usare whatever_collection<o_type> o whatever_collection<std::unique_ptr<o_type>> , che è un membro di un oggetto o una variabile automatica. Ovviamente, se non avessi bisogno di un numero dinamico di oggetti, potresti semplicemente utilizzare un array automatico di dimensioni fisse.

Né l'iterazione attraverso la raccolta né alcuna altra operazione sull'oggetto richiede una funzione di supporto per condividere la proprietà ... esso usa l'oggetto, quindi ritorna, e il chiamante garantisce che l'oggetto rimanga in vita per l'intera chiamata . Questo è di gran lunga il contratto più usato tra il chiamante e il chiamato.

Nicol Bolas ha commentato che "Se qualche oggetto tiene su un puntatore nudo e quell'oggetto muore ... oops". e "Gli oggetti devono garantire che l'oggetto viva attraverso la vita di quell'oggetto. Solo shared_ptr può farlo."

Non compro quell'argomento. Almeno non che shared_ptr risolva questo problema. Che dire:

  • Se una tabella hash contiene un oggetto e il codice hash dell'oggetto cambia ... oops.
  • Se alcune funzioni stanno iterando un vettore e un elemento viene inserito in quel vettore ... oops.

Come la garbage collection, l'uso predefinito di shared_ptr incoraggia il programmatore a non pensare al contratto tra gli oggetti, o tra la funzione e il chiamante. È necessario pensare a precondizioni e post-condizioni corrette e la durata dell'oggetto è solo un piccolo pezzo di quella torta più grande.

Gli oggetti non "muoiono", un pezzo di codice li distrugge. E lanciare shared_ptr al problema invece di capire il contratto di chiamata è una falsa sicurezza.

    
risposta data 04.02.2012 - 16:02
fonte
16

Preferisco non pensare in termini assoluti (come "last resort") ma relativamente al dominio del problema.

C ++ può offrire diversi modi per gestire la vita. Alcuni di loro cercano di ri-condurre gli oggetti in un modo guidato dallo stack. Alcuni altri cercano di sfuggire a questa limitazione. Alcuni sono "letterali", altri sono approssimativi.

In realtà puoi:

  1. usa la semantica del valore puro . Funziona per oggetti relativamente piccoli dove ciò che è importante sono "valori" e non "identità", dove puoi assumere che due Person con lo stesso name siano la stessa persona (meglio: due rappresentazioni di una stessa persona ). La durata è garantita dallo stack della macchina, la fine -essenziale- non è rilevante per il programma (poiché una persona è nome , non importa quale Person trasporta esso)
  2. usa oggetti allocati nello stack e riferimenti o puntatori correlati: consente il polimorfismo e garantisce la durata dell'oggetto. Non c'è bisogno di "puntatori intelligenti", poiché si garantisce che nessun oggetto possa essere "puntato" da strutture che rimangono nello stack più lunghe dell'oggetto a cui puntano (prima create l'oggetto, quindi le strutture che si riferiscono ad esso).
  3. usa oggetti allocati su heap gestiti dallo stack : questo è ciò che fanno std :: vector e tutti i contenitori, e wat std::unique_ptr fa (puoi pensare ad esso come un vettore con dimensione 1). Ancora una volta, ammetti che l'oggetto cominci ad esistere (e termina la sua esistenza) prima (dopo) la struttura dati a cui si riferiscono.

Il punto debole di questo mehtod è che i tipi di oggetto e le quantità non possono variare durante l'esecuzione di chiamate a livello di stack più profonde rispetto a dove sono state create. Tutte queste tecniche "falliscono" la loro forza in tutte le situazioni in cui la creazione e la cancellazione dell'oggetto sono conseguenza delle attività dell'utente, così che il tipo di runtime dell'oggetto non è noto in fase di compilazione e ci possono essere sovrastrutture che si riferiscono ad oggetti il l'utente chiede di rimuovere da una chiamata di funzione a livello di stack più profonda. In questo caso, devi:

  • introdurre qualche disciplina sulla gestione di oggetti e relative strutture di riferimento o ...
  • andare in qualche modo verso il lato oscuro di "sfuggire alla vita basata sullo stack puro": l'oggetto deve uscire indipendentemente dalle funzioni che li hanno creati. E deve lasciare ... finché non sono necessari .

C ++ isteslf non ha alcun meccanismo nativo per monitorare quell'evento ( while(are_they_needed) ), quindi devi approssimare con:

  1. usa proprietà condivisa : la vita degli oggetti è associata a un "contatore di riferimento": funziona se la "proprietà" può essere organizzata gerarchicamente, fallisce dove possono esistere loop di proprietà. Questo è ciò che std :: shared_ptr fa. E weak_ptr può essere utilizzato per interrompere il ciclo. Questo funziona la maggior parte del tempo, ma fallisce nel disegno di grandi dimensioni, dove molti designer lavorano in team diversi e non c'è una ragione chiara (qualcosa che deriva da un certo bisogno) su chi possiede cosa (l'esempio tipico sono catene a doppio gradimento: è il precedente a causa del successivo riferimento al precedente o al successivo proprietario del precedente riferimento al successivo? In caso contrario, le soluzioni sono equivalenti, e in un progetto di grandi dimensioni si rischia di mischiarle)
  2. Utilizza un heap di raccolta dei rifiuti : semplicemente non ti preoccupi della durata. Gestisci il collezionista volta per volta e ciò che è irrisolto è considerato "non più necessario" e ... beh ... ehm ... distrutto? finalizzato? congelato?. Esiste un certo numero di collector GC, ma non ne ho mai trovato uno che sia realmente consapevole del C ++. La maggior parte di loro libera memoria, non si preoccupa della distruzione degli oggetti.
  3. Utilizza un garbage collector consapevole di C ++ , con un'interfaccia dei metodi standard appropriata. Buona fortuna a trovarlo.

Passando alla prima soluzione all'ultimo, la quantità di struttura dati ausiliaria necessaria per gestire la vita degli oggetti aumenta, come il tempo speso per organizzarlo e mantenerlo.

Il garbage collector ha un costo, shared_ptr ha meno, unique_ptr ancora meno e lo stack degli oggetti gestiti ne ha pochissimi.

shared_ptr è l '"ultima risorsa" ?. No, non lo è: l'ultima risorsa sono i netturbini. shared_ptr è in realtà il std:: proposto l'ultima risorsa. Ma potrebbe essere la soluzione giusta, se sei nella situazione che ho spiegato.

    
risposta data 07.02.2012 - 09:55
fonte
9

L'unica cosa menzionata da Herb Sutter in una sessione successiva è che ogni volta che copi un shared_ptr<> c'è un incremento / decremento interbloccato che deve accadere. Nel codice multi-thread su un sistema multi-core, la sincronizzazione della memoria non è insignificante. Data la scelta, è meglio utilizzare un valore di stack o un unique_ptr<> e aggirare i riferimenti o i puntatori grezzi.

    
risposta data 04.02.2012 - 19:02
fonte
7

Non ricordo se l'ultimo "ricorso" fosse la parola esatta che usava, ma credo che il vero significato di ciò che disse fosse l'ultima "scelta": date chiare condizioni di proprietà; unique_ptr, weak_ptr, shared_ptr e anche i puntatori nudi hanno il loro posto.

Una cosa su cui tutti hanno concordato è che siamo (sviluppatori, autori di libri, ecc.) tutti nella "fase di apprendimento" di C ++ 11 e che i modelli e gli stili sono in via di definizione.

Ad esempio, Herb ha spiegato che dovremmo aspettarci nuove edizioni di alcuni dei libri seminali di C ++, come efficace C ++ (Meyers) e Standard di codifica C ++ (Sutter & Alexandrescu), un paio di anni fuori mentre il settore esperienza e best practice con C ++ 11 fuori.

    
risposta data 04.02.2012 - 17:32
fonte
5

Penso che quello che sta ottenendo è che sta diventando comune per tutti scrivere shared_ptr ogni volta che potrebbero aver scritto un puntatore standard (come una sorta di sostituzione globale), e che viene usato come cop-out invece di progettare effettivamente o almeno pianificando la creazione e l'eliminazione di oggetti.

L'altra cosa che le persone dimenticano (oltre al collo di bottiglia di blocco / aggiornamento / sblocco menzionato nel materiale sopra), è che shared_ptr da solo non risolve i problemi del ciclo. È ancora possibile perdere le risorse con shared_ptr:

Oggetto A, contiene un puntatore condiviso ad un altro Oggetto A L'oggetto B crea A a1 e A a2 e assegna a1.otherA = a2; e a2.otherA = a1; Ora, i puntatori condivisi dell'oggetto B utilizzati per creare a1, a2 escono dall'ambito (diciamo alla fine di una funzione). Ora hai una perdita - nessun altro fa riferimento a a1 e a2, ma si riferiscono l'un l'altro in modo che i loro conteggi ref siano sempre 1, e hai perso.

Questo è il semplice esempio, quando ciò accade nel codice reale accade di solito in modi complicati. C'è una soluzione con weak_ptr, ma così tante persone ora fanno solo shared_ptr ovunque e non sanno nemmeno del problema di perdita o anche di weak_ptr.

Per concludere: penso che i commenti a cui fa riferimento l'OP si riducano a questo:

Indipendentemente dalla lingua in cui lavori (gestita, non gestita o qualcosa di intermedio con conteggi di riferimento come shared_ptr), devi comprendere e decidere intenzionalmente sulla creazione, la durata e la distruzione dell'oggetto.

modifica: anche se questo significa "sconosciuto, ho bisogno di usare un shared_ptr", ci hai ancora pensato e lo stai facendo intenzionalmente.

    
risposta data 05.02.2012 - 22:22
fonte
2

Risponderò alla mia esperienza con Objective-C, una lingua in cui gli tutti gli oggetti sono conteggiati e allocati nell'heap. A causa del fatto di avere un modo di trattare gli oggetti, le cose sono molto più semplici per il programmatore. Ciò ha permesso di definire regole standard che, quando rispettate, garantiscono la robustezza del codice e nessuna perdita di memoria. Ha inoltre reso possibile l'intelligente ottimizzazione del compilatore per emergere come il recente ARC (conteggio automatico dei riferimenti).

Il mio punto è che shared_ptr dovrebbe essere la tua prima opzione piuttosto che l'ultima risorsa. Usa il conteggio dei riferimenti per impostazione predefinita e altre opzioni solo se sei sicuro di ciò che stai facendo. Sarai più produttivo e il tuo codice sarà più robusto.

    
risposta data 10.08.2012 - 18:28
fonte
1

Proverò a rispondere alla domanda:

How can we program without std::shared_ptr and still manage object lifetimes in safe way?

C ++ ha un gran numero di modi diversi di fare memoria, ad esempio:

  1. Utilizza struct A { MyStruct s1,s2; }; invece di shared_ptr nell'ambito della classe. Questo è solo per programmatori avanzati perché richiede di capire come funzionano le dipendenze e richiede la capacità di controllare le dipendenze quanto basta per limitarle a un albero. L'ordine delle classi nel file di intestazione è un aspetto importante di questo. Sembra che questo utilizzo sia già comune con i tipi nativi di c ++ incorporati, ma è usato con classi definite dal programmatore che sembra essere meno utilizzato a causa di questi problemi di dipendenza e ordine delle classi. Questa soluzione ha anche problemi con sizeof. I programmatori vedono i problemi in questo come un requisito per utilizzare dichiarazioni anticipate o #include non necessarie e quindi molti programmatori ricadranno su una soluzione inferiore di puntatori e successivamente su shared_ptr.
  2. Utilizza MyClass &find_obj(int i); + clone () anziché shared_ptr<MyClass> create_obj(int i); . Molti programmatori vogliono creare fabbriche per creare nuovi oggetti. shared_ptr è ideale per questo tipo di utilizzo. Il problema è che presuppone già una soluzione di gestione della memoria complessa che utilizza l'allocazione dell'heap / free store, anziché una soluzione più semplice basata su stack o object based. Una buona gerarchia di classi C ++ supporta tutti gli schemi di gestione della memoria, non solo uno di essi. La soluzione basata su riferimento può funzionare se l'oggetto restituito è memorizzato all'interno dell'oggetto contenitore, invece di utilizzare la variabile dell'ambito della funzione locale. È necessario evitare di passare la proprietà dal codice di fabbrica a quello dell'utente. Copiare l'oggetto dopo aver usato find_obj () è un buon modo per gestirlo: normali costruttori di copie e normali costruttori (di classi diverse) con parametro di refrerence o clone () per oggetti polimorfi possono gestirlo.
  3. Uso di riferimenti anziché puntatori o shared_ptrs. Ogni classe c ++ ha costruttori e ogni membro dei dati di riferimento deve essere inizializzato. Questo utilizzo può evitare molti usi di puntatori e shared_ptrs. Hai solo bisogno di scegliere se la tua memoria è all'interno dell'oggetto, o al di fuori di esso, e scegliere la soluzione struct o la soluzione di riferimento in base alla decisione. I problemi con questa soluzione sono in genere correlati all'elusione dei parametri del costruttore, pratica comune ma problematica e incomprensione su come dovrebbero essere progettate le interfacce per le classi.
risposta data 04.02.2012 - 20:05
fonte

Leggi altre domande sui tag