2015-07-24 20 views
17

Grazie a std::make_shared, mi chiedo se il costruttore per std::shared_ptr, che accetta un puntatore raw, abbia alcun valore tranne quando si interfaccia con il codice legacy/library, ad es. quando si memorizza l'output di una fabbrica.Quando vorrei creare un puntatore condiviso da un puntatore raw

  • Esistono altri casi d'uso legittimi?
  • È un consiglio ragionevole evitare questo costruttore?
  • Anche per quanto riguarda l'estensione del controllo dei codici per questo posto che avvisa il programmatore ogni volta che viene utilizzato?
  • Le stesse linee guida (qualunque esse siano) si applicano a shared_ptr<T>::reset(T*)?

Per quanto riguarda i controlli di codice: So che l'interfacciamento con il codice legacy/biblioteca è abbastanza comune, codice controlla in modo automatizzato potrebbe essere problematico, ma nella maggior parte dei casi che ho incontrato finora, preferisco utilizzare un unique_ptr comunque e inoltre non sto parlando di un avviso del compilatore che viene visualizzato su -Wall, ma piuttosto su una regola per l'analisi del codice statico durante la revisione del codice.


La mia motivazione:
E 'relativamente facile dire "Non usare std::shared_ptr<T>(new T(...)), preferisco sempre std::make_shared<T>(...)" (che credo sia corretto consigliare?). Ma mi chiedo se non sia un odore di design in generale, se si deve creare un shared_ptr da un puntatore raw, anche - o soprattutto - se l'oggetto non è stato appena creato tramite new, perché l'oggetto avrebbe dovuto essere creato in primo luogo come oggetto "condiviso" o "unico".

+2

Cosa succede se si desidera una classe shared-pointer-to-base e si desidera inizializzarla con una classe raw-pointer-to-derived? –

+0

@ TheParamagneticCroissant: ciò può essere ottenuto con 'make_shared', no? – MikeMB

+2

Non credo che usare ciecamente "make_shared" sia un consiglio corretto. Tenere presente che con rendere condiviso la memoria allocata viene rilasciata quando entrambi i contatori * e * deboli condivisi raggiungono lo zero. Questo è il prezzo dell'utilizzo di una singola allocazione per l'oggetto e il blocco di controllo. – sbabbi

risposta

0

Molte delle risposte forniscono almeno un aspetto unico, quindi ho deciso di fare una risposta sommaria. Il merito va a @Niall, @Chris Drew, @utnapistim, @isanae, @Markus Mayr e (per procura) Scott Meyers.

ragioni per le quali non si potrebbe desiderare/potrebbe non essere in grado di utilizzare make_shared sono:

  • Devi usare un deleter costume e/o anche un allocatore personalizzato/nuovo operatore.
    Questa è probabilmente la ragione più comune, soprattutto quando si interfaccia con librerie di terze parti. std::allocate_shared potrebbe aiutare in alcune di queste situazioni.
  • Se la memoria è una preoccupazione e spesso si hanno indicatori deboli che sopravvivono all'oggetto.
    Poiché l'oggetto gestito viene creato sullo stesso blocco di memoria del blocco di controllo, la memoria non può essere liberata finché anche l'ultimo puntatore debole non viene distrutto.
  • Se si dispone di un costruttore privato e ad es. solo una funzione di fabbrica pubblica.
    Fare un make_shared un amico non è una soluzione valida in quel caso, poiché la costruzione effettiva potrebbe accadere in qualche funzione/classe di supporto.

Per quanto riguarda i controlli di codice, le eccezioni di cui sopra sono probabilmente troppi per un controllo automatico di un "Usa make_shared quando possibile" linea guida, così come per chiamare una violazione di tale linea guida un odore design nella sua propria ragione.

Come un punto luminoso all'orizzonte, anche se una libreria richiede funzioni di allocatore e deallocator personalizzate e non possiamo/non vogliamo usare std::allocate_shared, possiamo creare la nostra versione di make shared che almeno incapsula il assegnazione e cancellazione chiamata e aumenta la sicurezza delle eccezioni (anche se molto probabilmente non offre il vantaggio di allocazione singola).

14

Il primo caso di utilizzo che viene in mente è quando il deleter non è il valore predefinito delete.

E.g. in un ambiente Windows gli oggetti COM devono essere utilizzati alcune volte, il rilascio di questi oggetti deve essere eseguito sull'oggetto stesso, tramite Release. È possibile utilizzare ATL, ma non tutti vogliono usarlo.

struct ReleaseCom { 
    template <class T> 
    void operator() (T* p) const 
    { 
    p->Release(); 
    } 
}; 
IComInterface* p = // co created or returned as a result 
std::share_ptr<IComInterface> sp(p, ReleaseCom()); 

Un altro raro situazione - ma ancora valido - è quando un oggetto (di gestire o anche la memoria crudo) è personalizzato allocati in una DLL, sistema operativo o biblioteca e ha la sua propria funzione di pulizia associato che deve essere chiamato (che può o non può chiamare a turno delete). Se è coinvolta l'allocazione di memoria, std::allocate_shared offre un controllo più avanzato sull'allocatore che deve essere utilizzato senza esporre un puntatore non elaborato.

La mia sensazione personale è quella data std::make_shared e std::allocate_shared, l'obbligo di costruire un shared_ptr da puntatori grezzi sta diventando sempre meno. Anche i casi di cui sopra, possono essere integrati in funzioni di allocazione e gestione dell'utilità, rimuovendo dal codice di business logic principale.

+0

Giusto, completamente dimenticato del deleter costumabile. Quindi sarebbe solo il singolo costruttore di argomenti che dovrebbe essere evitato. – MikeMB

+0

@MikeMB. In generale, sì. Ma detto questo, ci sono situazioni in cui è richiesta l'eliminazione "normale", ma il codice che fa l'allocazione non è modificabile - si ottiene solo un oggetto già assegnato e si ha bisogno di gestirlo. Penso che il consiglio sia preferibile a 'make_shared', ma il costruttore è ancora necessario perché' make_shared' non sempre copre tutti i casi d'uso. – Niall

+0

'std :: allocate_shared' fornisce un'altra alternativa evitando sia il puntatore non protetto che il delet predefinito. – Potatoswatter

5

• Esistono altri casi d'uso legittimi?

Sì: c'è una situazione in cui una risorsa non mappa di new/delete:

handle_type APIXCreateHandle(); // third party lib 
void APIXDestroyHandle(handle_type h); // third party lib 

In questo caso, si vuole utilizzare direttamente il costruttore.

• È ragionevole evitare questo costruttore?

Quando la funzionalità si sovrappone a make_shared, sì.

• Le stesse linee guida (qualunque esse siano) si applicano a shared_ptr :: reset (T *)?

Penso che non dovrebbero. Se lo desideri, puoi sostituire la chiamata di reset con un'assegnazione con il risultato di una chiamata std :: make_shared. Preferirei vedere invece la chiamata di reset - sarebbe più esplicito nelle intenzioni.

Per quanto riguarda i controlli di codice: so che l'interfacciamento con il codice legacy/biblioteca è abbastanza comune

consideri avvolgendo la libreria di terze parti in un'interfaccia che garantisce i valori restituiti sono unique_ptr-avvolto. Ciò fornirà un punto di centralizzazione e l'opportunità di altre ottimizzazioni di convenienza/sicurezza.

È relativamente facile dire [...] (Quale credo sia corretto consigliare?). Ma mi chiedo se non si tratti di un odore di design in generale

Non è l'odore del design usarlo; solo per usarlo quando std :: make_shared/std :: make_unique funzionerebbe altrettanto bene.

Edit: per affrontare il punto della questione: probabilmente non in grado di aggiungere una regola statica-analisi per questo, a meno che anche aggiunge un elenco di convenzione/eccezioni ad esso (vale a dire "tranne che per hanno eliminato personalizzati e i livelli di adattamento dell'API lib di terze parti, make_shared e make_unique devono sempre essere utilizzati "). Tale regola verrebbe probabilmente ignorata per alcuni dei tuoi file.

Quando si utilizza i costruttori direttamente, però, può essere una buona idea per averli in una funzione dedicata proprio a questo (simile a quello che make_unique e make_shared fanno):

namespace api_x 
{ 
    std::shared_ptr<handle_type> make_handle(); // calls APIXCreateHandle internally 
               // also calls the constructor 
               // to std::shared_ptr 
} 
9

Scott Meyers elenca una un paio di eccezioni in Effective Modern C++

Il primo è già stato menzionato. Se è necessario fornire un deleter personalizzato non è possibile utilizzare make_shared.

Il secondo è se si è in un sistema con preoccupazioni memoria e si assegnano un oggetto molto grande quindi utilizzando make_shared alloca un blocco per l'oggetto e il blocco di controllo che comprende il conteggio di riferimenti debole. Se si dispone di uno weak_ptr s che punta all'oggetto, la memoria non può essere deallocata. D'altra parte se non hai usato make_shared allora non appena l'ultimo shared_ptr viene cancellato, la memoria per l'oggetto molto grande può essere deallocata.

3

Utilizzare std::make_shared ogni volta che è possibile.

ci sono due motivi non si può ottenere via con esso, però:

  • Si desidera utilizzare un deleter personalizzato per l'oggetto. Questo potrebbe essere il caso se si interfaccia con codice che non utilizza RAII. Altre risposte forniscono maggiori dettagli.

  • Hai un nuovo operatore personalizzato che desideri chiamare. std::make_shared non può chiamarlo per te, perché il suo scopo è di allocare memoria una volta (anche se non richiesto dallo standard). Vedi http://ideone.com/HjmFl1.

Il ragionamento dietro std::make_shared non è che si evita la parola chiave new, ma che si evita un'allocazione di memoria. Un puntatore condiviso deve impostare una struttura di dati di controllo. Pertanto, quando si imposta shared_ptr senza make_shared, è necessario eseguire due allocazioni: una per l'oggetto, un'altra per la struttura di controllo. make_shared (di solito) assegna solo un (più grande) blocco di memoria e costruisce l'oggetto contenuto e la struttura di controllo in posizione. Il motivo alla base di make_shared è (inizialmente era) che è più efficiente nella maggior parte dei casi, non che la sintassi sia più carina. Questo è anche il motivo per cui non c'era lo std::make_unique in C++ 11. (Come ChrisDrew sottolineato, ho suggerito che std::make_unique è stato aggiunto solo per simmetria/sintassi più bella. Può aiutare a scrivere eccezione il codice di sicurezza in modo più compatto, però, vedere Herb Sutter's GotW #102 o this question.)

+2

Hai ragione circa il motivo originale per 'make_shared' ma evitando la parola chiave' new' non si tratta solo di fare "la sintassi più carino "rende anche più un codice eccezionalmente sicuro. E _that_ è la ragione principale per cui "make_unique" è stato incluso in C++ 14. –

+1

@ChrisDrew, hai ragione. Grazie. –

3

In aggiunta alle altre risposte , non è possibile utilizzare make_shared se il costruttore è privato, ad esempio da una funzione di fabbrica.

class C 
{ 
public: 
    static std::shared_ptr<C> create() 
    { 
     // fails 
     // return std::make_shared<C>(); 

     return std::shared_ptr<C>(new C); 
    } 
private: 
    C(); 
}; 
Problemi correlati