Verschil in make_shared en normaal shared_ptr in C++

Er zijn veel Google- en stackoverflow-berichten hierover, maar ik begrijp niet waarom make_sharedefficiënter is dan het rechtstreeks gebruiken van shared_ptr.

Kan iemand me stap voor stap uitleggen hoe de objecten die door beide zijn gemaakt en de bewerkingen worden uitgevoerd, zodat ik kan begrijpen hoe make_sharedefficiënt is. Ik heb hierboven een voorbeeld gegeven ter referentie.


Antwoord 1, autoriteit 100%

Het verschil is dat std::make_sharedéén heap-toewijzing uitvoert, terwijl het aanroepen van de std::shared_ptrconstructor er twee uitvoert.

Waar vinden de heap-toewijzingen plaats?

std::shared_ptrbeheert twee entiteiten:

  • het controleblok (slaat metagegevens op zoals ref-counts, type gewist deleter, enz.)
  • het object dat wordt beheerd

std::make_sharedvoert een enkele heap-toewijzing uit voor de ruimte die nodig is voor zowel het besturingsblok als de gegevens. In het andere geval roept new Obj("foo")een heap-toewijzing op voor de beheerde data en voert de std::shared_ptrconstructor een andere uit voor het controleblok.

Bekijk voor meer informatie de implementatieopmerkingenop cppreference .

Update I: Exception-Safety

OPMERKING (2019/08/30): Dit is geen probleem sinds C++17, vanwege de veranderingen in de evaluatievolgorde van functieargumenten. In het bijzonder moet elk argument voor een functie volledig worden uitgevoerd voordat andere argumenten worden geëvalueerd.

Omdat de OP zich lijkt af te vragen wat de uitzonderingsveiligheid is, heb ik mijn antwoord bijgewerkt.

Beschouw dit voorbeeld,

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")));

Omdat C++ willekeurige volgorde van evaluatie van subdeksels mogelijk maakt, is een mogelijke bestellingen:

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

Stel nu, veronderstel dat we een uitzondering krijgen bij stap 2 (bijvoorbeeld buiten geheugenuitzondering, RhsConstructor gooide een uitzondering op). Vervolgens verliezen we het geheugen dat bij stap 1 is toegewezen, omdat er niets een kans heeft gehad om het op te ruimen. De kern van het probleem hier is dat de onbewerkte aanwijzer niet is doorgegeven aan de std::shared_ptrconstructor onmiddellijk.

Een manier om dit op te lossen is om ze op afzonderlijke regels te doen, zodat deze willekeurige bestellingen niet kan plaatsvinden.

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

De gewenste manier om dit natuurlijk op te lossen, is het gebruik van std::make_sharedin plaats daarvan.

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

Update II: Nadeel van std::make_shared

Citattering Casey Opmerkingen:

Aangezien er slechts één toewijzing is, kan het geheugen van de Poinee niet worden gehandeld totdat het bedieningsblok niet meer in gebruik is. Een weak_ptrkan het bedieningsblok voor onbepaalde tijd levend houden.

Waarom houden instanties van weak_ptrs het controleblok in leven?

Er moet een manier zijn voor weak_ptrs om te bepalen of het beheerde object nog steeds geldig is (bijv. voor lock). Ze doen dit door het aantal shared_ptr‘s te controleren die eigenaar zijn van het beheerde object, dat is opgeslagen in het controleblok. Het resultaat is dat de controleblokken in leven zijn totdat het aantal shared_ptren het aantal weak_ptrbeide 0 bereiken.

Terug naar std::make_shared

Aangezien std::make_sharedeen enkele heap-toewijzing maakt voor zowel het controleblok als het beheerde object, is er geen manier om het geheugen voor het controleblok en het beheerde object onafhankelijk van elkaar vrij te maken. We moeten wachten tot we zowel het controleblok als het beheerde object kunnen bevrijden, wat toevallig is totdat er geen shared_ptrs of weak_ptrs in leven zijn.

Stel dat we in plaats daarvan twee heap-toewijzingen hebben uitgevoerd voor het besturingsblok en het beheerde object via de constructor newen shared_ptr. Dan maken we het geheugen vrij voor het beheerde object (misschien eerder) als er geen shared_ptrs in leven zijn, en maken we het geheugen vrij voor het controleblok (misschien later) als er geen weak_ptrleeft.


Antwoord 2, autoriteit 7%

De gedeelde aanwijzer beheert zowel het object zelf als een klein object dat de referentietelling en andere huishoudelijke gegevens bevat. make_sharedkan een enkel geheugenblok toewijzen om beide te bewaren; het construeren van een gedeelde aanwijzer van een aanwijzer naar een reeds toegewezen object moet een tweede blok toewijzen om de referentietelling op te slaan.

Evenals dit efficiency, met make_sharedbetekent dat u niet hoeft om te gaan met newen ruwe pointers helemaal niet, waardoor een betere uitzondering veiligheid – er is geen Mogelijkheid om een ​​uitzondering te gooien nadat het object is toebrengt, maar alvorens toe te wijzen aan de Smart Pointer.


Antwoord 3, Autoriteit 6%

Er is een ander geval waarin de twee mogelijkheden verschillen, bovenop die al genoemd: als u een niet-openbare constructeur (beschermd of privé) moet noemen, kan make_shared niet toegang hebben tot de variant Nieuwe werkt prima.

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){}
};

Antwoord 4, Autoriteit 2%

Als u speciale geheugenuitlijning nodig hebt op het object dat wordt gecontroleerd door Shared_PTR, kunt u niet vertrouwen op make_shared, maar ik denk dat het de enige goede reden is om het niet te gebruiken.


Antwoord 5, Autoriteit 2%

Ik zie een probleem met std :: make_shared, het ondersteunt geen privé / beschermde constructeurs


Antwoord 6

Shared_ptr: voert twee hooptoewijzing uit

  1. Controleblok (referentietelling)
  2. Object wordt beheerd

make_shared: voert slechts één hoop toewijzing uit

  1. Controleblok en objectgegevens.

Antwoord 7

Ik denk dat het uitzonderingsveiligheidsonderdeel van het antwoord van de heer MPark nog steeds een geldige zorg is. Bij het maken van een Shared_PTR vindt dit: Shared_PTR & LT; T & GT; (nieuw t), het nieuwe t slagen, terwijl de toewijzing van het controleblok van de Shared_PTR kan mislukken. In dit scenario zal de nieuw toegewezen T lekken, aangezien de Shared_PTR geen manier is om te weten dat het in de plaats is gemaakt en het is veilig om het te verwijderen. of ik mis iets? Ik denk niet dat de strengere regels inzake functie-parameterevaluatie op enigerlei wijze helpen …


Antwoord 8

Over efficiëntie en goedgekeurde tijd besteed aan toewijzing, ik heb deze eenvoudige test hieronder gemaakt, ik heb vele gevallen door deze twee manieren gemaakt (één per keer):

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));
}

Het ding is, met behulp van make_shared nam de dubbele tijd in vergelijking met het gebruik van nieuw. Dus, met behulp van nieuwe er zijn twee hoop toewijzingen in plaats van één met make_shared. Misschien is dit een stomme test, maar laat het niet zien dat het gebruik van make_shared meer tijd kost dan het gebruik van nieuwe? Natuurlijk heb ik het over alleen gebruikte tijd.

Other episodes