Differenza in make_shared e normale shared_ptr in C ++


276
std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

Ci sono molti post su Google e StackOverflow, ma non riesco a capire perché make_sharedsia più efficiente dell'utilizzo diretto shared_ptr.

Qualcuno può spiegarmi passo dopo passo la sequenza di oggetti creati e le operazioni eseguite da entrambi in modo che io possa capire quanto make_sharedsia efficiente. Ho dato un esempio sopra per riferimento.


4
Non è più efficiente. Il motivo per usarlo è per la sicurezza delle eccezioni.
Yuushi,

Alcuni articoli dicono, evita un certo sovraccarico di costruzione, puoi per favore spiegare di più su questo?
Anup Buchke,

16
@Yuushi: la sicurezza delle eccezioni è un buon motivo per usarla, ma è anche più efficiente.
Mike Seymour,

3
32:15 è dove inizia nel video che ho collegato sopra, se questo aiuta.
chris,

4
Vantaggio di stile di codice minore: usando make_sharedpuoi scrivere auto p1(std::make_shared<A>())e p1 avrà il tipo corretto.
Ivan Vergiliev,

Risposte:


333

La differenza è che std::make_sharedesegue un'allocazione di heap, mentre chiamare il std::shared_ptrcostruttore ne esegue due.

Dove avvengono le allocazioni di heap?

std::shared_ptr gestisce due entità:

  • il blocco di controllo (memorizza metadati come ref-count, deleter cancellati dal tipo, ecc.)
  • l'oggetto da gestire

std::make_sharedesegue una singola contabilità di allocazione dell'heap per lo spazio necessario sia per il blocco di controllo che per i dati. Nell'altro caso, new Obj("foo")invoca un'allocazione di heap per i dati gestiti e il std::shared_ptrcostruttore ne esegue un altro per il blocco di controllo.

Per ulteriori informazioni, consultare le note di implementazione su cppreference .

Aggiornamento I: Eccezione-Sicurezza

NOTA (2019/08/30) : questo non è un problema dal C ++ 17, a causa delle modifiche nell'ordine di valutazione degli argomenti delle funzioni. In particolare, ogni argomento di una funzione deve essere eseguito completamente prima della valutazione di altri argomenti.

Dato che l'OP sembra interrogarsi sul lato della sicurezza delle eccezioni, ho aggiornato la mia risposta.

Considera questo esempio,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

Poiché C ++ consente un ordine arbitrario di valutazione delle sottoespressioni, un possibile ordinamento è:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

Supponiamo ora che venga generata un'eccezione al passaggio 2 (ad esempio, eccezione di memoria insufficiente, il Rhscostruttore ha generato un'eccezione). Perdiamo quindi la memoria allocata al passaggio 1, poiché nulla avrà avuto la possibilità di ripulirla. Il nocciolo del problema qui è che il puntatore non elaborato non è stato passato std::shared_ptrimmediatamente al costruttore.

Un modo per risolvere questo problema è eseguirli su linee separate in modo che questo ordinamento arbitrale non possa avvenire.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

Il modo preferito per risolvere questo ovviamente è usare std::make_sharedinvece.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Aggiornamento II: svantaggio di std::make_shared

Citando i commenti di Casey :

Poiché esiste una sola allocazione, la memoria del puntatore non può essere deallocata fino a quando il blocco di controllo non è più in uso. A weak_ptrpuò mantenere il blocco di controllo in vita indefinitamente.

Perché le istanze di weak_ptrs mantengono attivo il blocco di controllo?

Deve esserci un modo per weak_ptrs per determinare se l'oggetto gestito è ancora valido (es. Per lock). Lo fanno controllando il numero di shared_ptrs che possiedono l'oggetto gestito, che è memorizzato nel blocco di controllo. Il risultato è che i blocchi di controllo sono attivi fino a quando il shared_ptrconteggio e il weak_ptrconteggio non raggiungono entrambi 0.

Torna a std::make_shared

Poiché std::make_sharedeffettua una singola allocazione di heap sia per il blocco di controllo che per l'oggetto gestito, non è possibile liberare la memoria per il blocco di controllo e l'oggetto gestito in modo indipendente. Dobbiamo aspettare fino possiamo liberare sia il blocco di controllo e l'oggetto gestito, che risulta essere fino a quando non ci sono shared_ptrs o weak_ptrè vivo.

Supponiamo che abbiamo invece eseguito due allocazioni di heap per il blocco di controllo e l'oggetto gestito tramite newe shared_ptrcostruttore. Quindi liberiamo la memoria per l'oggetto gestito (forse prima) quando non ci sono shared_ptrs in vita, e liberiamo la memoria per il blocco di controllo (forse più tardi) quando non ci sono weak_ptrs in vita.


53
È una buona idea menzionare anche il piccolo make_sharedaspetto negativo di un caso angolare : poiché esiste solo un'allocazione, la memoria del puntatore non può essere deallocata fino a quando il blocco di controllo non è più in uso. A weak_ptrpuò mantenere il blocco di controllo in vita indefinitamente.
Casey,

14
Un altro punto, più stilistico, è: se usi make_sharede in modo make_uniquecoerente, non avrai i puntatori grezzi che puoi considerare ogni ricorrenza newcome un odore di codice.
Philipp,

6
Se solo un non c'è shared_ptr, e non weak_ptrs, chiamando reset()sulla shared_ptristanza eliminare il blocco di controllo. Ma questo è indipendentemente o se è make_sharedstato utilizzato. L'uso make_sharedfa la differenza perché potrebbe prolungare la durata della memoria allocata per l'oggetto gestito . Quando il shared_ptrconteggio raggiunge 0, il distruttore per l'oggetto gestito viene chiamato indipendentemente da make_shared, ma liberando la sua memoria può essere fatto solo se nonmake_shared è stato usato. Spero che questo lo renda più chiaro.
mpark

4
Vale anche la pena ricordare che make_shared può sfruttare l'ottimizzazione "We Know Where You Live" che consente al blocco di controllo di essere più piccolo di un puntatore. (Per i dettagli, vedere la presentazione GN2012 di Stephan T. Lavavej al minuto 12. circa) make_shared non solo evita un'allocazione, ma alloca anche meno memoria totale.
KnowItAllWannabe,

1
@HannaKhalil: è forse questo il regno di ciò che stai cercando ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark

26

Il puntatore condiviso gestisce sia l'oggetto stesso, sia un piccolo oggetto contenente il conteggio dei riferimenti e altri dati di pulizia. make_sharedpuò allocare un singolo blocco di memoria per contenere entrambi; la costruzione di un puntatore condiviso da un puntatore a un oggetto già allocato dovrà allocare un secondo blocco per memorizzare il conteggio dei riferimenti.

Oltre a questa efficienza, l'utilizzo make_sharedsignifica che non è necessario affrontarenew i puntatori non elaborati, offrendo una maggiore sicurezza delle eccezioni: non è possibile generare un'eccezione dopo aver allocato l'oggetto ma prima di assegnarlo al puntatore intelligente.


2
Ho capito bene il tuo primo punto. Potete per favore elaborare o fornire alcuni collegamenti sul secondo punto sulla sicurezza delle eccezioni?
Anup Buchke

22

C'è un altro caso in cui le due possibilità differiscono, oltre a quelle già menzionate: se devi chiamare un costruttore non pubblico (protetto o privato), make_shared potrebbe non essere in grado di accedervi, mentre la variante con la nuova funziona bene .

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};

Ho riscontrato questo esatto problema e newho deciso di utilizzare , altrimenti avrei usato make_shared. Ecco una domanda correlata a riguardo: stackoverflow.com/questions/8147027/… .
jigglypuff

6

Se hai bisogno di un allineamento di memoria speciale sull'oggetto controllato da shared_ptr, non puoi fare affidamento su make_shared, ma penso che sia l'unica buona ragione per non usarlo.


2
Una seconda situazione in cui make_shared è inappropriata è quando si desidera specificare un deleter personalizzato.
KnowItAllWannabe,

5

Vedo un problema con std :: make_shared, non supporta costruttori privati ​​/ protetti


3

Shared_ptr: Esegue due allocazioni di heap

  1. Blocco di controllo (conteggio dei riferimenti)
  2. Oggetto gestito

Make_shared: Esegue una sola allocazione di heap

  1. Blocco di controllo e dati oggetto.

0

Per quanto riguarda l'efficienza e il tempo dedicato all'allocazione, ho fatto questo semplice test di seguito, ho creato molti casi in questi due modi (uno alla volta):

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

Il fatto è che l'utilizzo di make_shared ha richiesto il doppio tempo rispetto all'utilizzo di new. Quindi, usando new ci sono due allocazioni di heap invece di una che usa make_shared. Forse questo è un test stupido ma non dimostra che l'uso di make_shared richiede più tempo rispetto all'utilizzo di new? Certo, sto parlando solo del tempo usato.


4
Quel test è in qualche modo inutile. Il test è stato eseguito nella configurazione del rilascio con ottimizzazioni scoperte? Inoltre, tutti i tuoi articoli vengono liberati immediatamente, quindi non è realistico.
Phil1970,

0

Penso che la parte relativa alla sicurezza delle eccezioni della risposta dell'onorevole mpark sia ancora una preoccupazione valida. durante la creazione di shared_ptr in questo modo: shared_ptr <T> (nuova T), la nuova T potrebbe avere esito positivo, mentre l'allocazione del blocco di controllo di shared_ptr potrebbe non riuscire. in questo scenario, la nuova T allocata perderà, dal momento che shared_ptr non ha modo di sapere che è stata creata sul posto ed è sicuro eliminarla. Oppure mi sfugge qualcosa? Non penso che le regole più rigorose sulla valutazione dei parametri di funzione siano di alcun aiuto qui ...

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.