Różnica w make_shared i normal shared_ptr w C ++

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

Wiele postów z Google i Stackoverflow jest na ten temat, ale nie jestem w stanie zrozumieć, dlaczego make_sharedjest bardziej wydajny niż bezpośrednie korzystanie shared_ptr.

Czy ktoś może mi wyjaśnić krok po kroku sekwencję obiektów utworzonych i operacji wykonanych przez oba, aby móc zrozumieć, jak make_sharedjest skuteczny. Podałem jeden przykład powyżej w celach informacyjnych.

Anup Buchke
źródło
4
To nie jest bardziej wydajne. Powodem jego użycia jest wyjątkowe bezpieczeństwo.
Yuushi,
Niektóre artykuły mówią, że unika się pewnych kosztów konstrukcyjnych, czy możesz wyjaśnić więcej na ten temat?
Anup Buchke
16
@Yuushi: Wyjątkowe bezpieczeństwo jest dobrym powodem do korzystania z niego, ale jest również bardziej wydajne.
Mike Seymour
3
32:15 jest miejscem, w którym zaczyna się w filmie, który zamieściłem powyżej, jeśli to pomaga.
Chris
4
Niewielka zaleta stylu kodu: używając make_sharedmożesz pisać, auto p1(std::make_shared<A>())a p1 będzie miał poprawny typ.
Ivan Vergiliev

Odpowiedzi:

333

Różnica polega na tym, że std::make_sharedwykonuje jeden przydział sterty, podczas gdy wywoływanie std::shared_ptrkonstruktora wykonuje dwa.

Gdzie mają miejsce alokacje sterty?

std::shared_ptr zarządza dwoma podmiotami:

  • blok kontrolny (przechowuje metadane, takie jak liczba zliczeń, kasowanie danych z kasowaniem typu itp.)
  • obiekt zarządzany

std::make_sharedwykonuje pojedynczą alokację sterty z uwzględnieniem miejsca potrzebnego zarówno dla bloku sterującego, jak i danych. W innym przypadku new Obj("foo")wywołuje alokację sterty dla zarządzanych danych, a std::shared_ptrkonstruktor wykonuje kolejną dla bloku sterującego.

Aby uzyskać więcej informacji, sprawdź uwagi dotyczące implementacji na cppreference .

Aktualizacja I: Bezpieczeństwo wyjątkowe

UWAGA (2019/08/30) : Nie stanowi to problemu od C ++ 17, ze względu na zmiany w kolejności oceny argumentów funkcji. W szczególności każdy argument funkcji jest wymagany do pełnego wykonania przed oceną innych argumentów.

Ponieważ PO wydaje się zastanawiać nad kwestią bezpieczeństwa i wyjątków, zaktualizowałem swoją odpowiedź.

Rozważ ten przykład

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

Ponieważ C ++ pozwala na dowolną kolejność oceny podwyrażeń, jednym z możliwych porządków jest:

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

Załóżmy, że otrzymamy wyjątek zgłoszony w kroku 2 (np. Z wyjątku pamięci, Rhskonstruktor zgłosił wyjątek). Następnie tracimy pamięć przydzieloną w kroku 1, ponieważ nic nie będzie miało szansy jej wyczyszczenia. Istotą tego problemu jest to, że surowy wskaźnik nie został natychmiast przekazany do std::shared_ptrkonstruktora.

Jednym ze sposobów, aby to naprawić, jest wykonanie ich w osobnych wierszach, aby to arbitralne zamówienie nie mogło wystąpić.

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

Preferowanym sposobem rozwiązania tego jest oczywiście użycie std::make_sharedzamiast tego.

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

Aktualizacja II: Wada std::make_shared

Cytując komentarze Casey :

Ponieważ istnieje tylko jeden przydział, pamięci osoby, której cel jest przyznany, nie można zwolnić, dopóki blok kontrolny nie będzie już używany. A weak_ptrmoże utrzymywać blok kontrolny przy życiu przez czas nieokreślony.

Dlaczego instancje weak_ptrs utrzymują blok kontrolny przy życiu?

Musi istnieć sposób, aby weak_ptrs określił, czy zarządzany obiekt jest nadal ważny (np. Dla lock). Robią to, sprawdzając liczbę shared_ptrs, która jest właścicielem zarządzanego obiektu, który jest przechowywany w bloku kontrolnym. Powoduje to, że bloki kontrolne są aktywne, dopóki shared_ptrliczba i weak_ptrliczba nie osiągną 0.

Wrócić do std::make_shared

Ponieważ std::make_shareddokonuje pojedynczej alokacji sterty zarówno dla bloku sterującego, jak i obiektu zarządzanego, nie ma możliwości niezależnego zwolnienia pamięci dla bloku sterującego i obiektu zarządzanego. Musimy poczekać, aż możemy uwolnić zarówno blok sterowania i zarządzanego obiektu, który dzieje się dopóki nie ma shared_ptrs lub weak_ptrS żyje.

Załóżmy, że zamiast tego wykonaliśmy dwie alokacje sterty dla bloku sterującego i obiektu zarządzanego za pośrednictwem newi shared_ptrkonstruktora. Następnie zwalniamy pamięć dla zarządzanego obiektu (może wcześniej), gdy nie ma shared_ptrżywych, i zwalniamy pamięć dla bloku kontrolnego (może później), gdy nie ma weak_ptrżywych.

mpark
źródło
53
Warto również wspomnieć o drobnych wadach tego narożnika make_shared: ponieważ istnieje tylko jedna alokacja, pamięci osoby, której dane dotyczą, nie można zwolnić, dopóki blok sterowania nie będzie już używany. A weak_ptrmoże utrzymywać blok kontrolny przy życiu przez czas nieokreślony.
Casey
14
Kolejnym, bardziej stylistycznym punktem jest: Jeśli używasz make_sharedi make_uniquekonsekwentnie, nie będziesz posiadał surowych wskaźników i możesz traktować każde wystąpienie newjako zapach kodu.
Filip
6
Jeśli jest tylko jeden shared_ptri nie ma weak_ptrs, nazywając reset()na shared_ptrprzykład usunie blok sterowania. Ale to niezależnie od tego, czy make_sharedzostało użyte. Używanie make_sharedrobi różnicę, ponieważ może przedłużyć żywotność pamięci przydzielonej dla zarządzanego obiektu . Kiedy shared_ptrliczba osiągnie wartość 0, destruktor zarządzanego obiektu jest wywoływany niezależnie od make_shared, ale zwolnienie jego pamięci można wykonać tylko wtedy, gdy niemake_shared był używany. Mam nadzieję, że to wyjaśnia.
mpark
4
Warto również wspomnieć, że make_shared może skorzystać z optymalizacji „We Know Where You Live”, która pozwala, aby blok kontrolny był mniejszy. (Aby uzyskać szczegółowe informacje, zobacz prezentację GN2012 Stephana T. Lavaveja około 12. minuty). Make_shared nie tylko pozwala uniknąć przydziału, ale także przydziela mniej pamięci.
KnowItAllWannabe
1
@HannaKhalil: Czy to może jest to, czego szukasz ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

Współdzielony wskaźnik zarządza zarówno samym obiektem, jak i małym obiektem zawierającym liczbę referencji i inne dane porządkowe. make_sharedmoże przydzielić jeden blok pamięci do przechowywania obu tych elementów; zbudowanie współdzielonego wskaźnika ze wskaźnika do już przydzielonego obiektu będzie wymagało przydzielenia drugiego bloku do przechowywania liczby referencji.

Oprócz tej wydajności użycie make_sharedoznacza, że ​​nie musisz w ogóle zajmować się newsurowymi wskaźnikami, co zapewnia większe bezpieczeństwo wyjątków - nie ma możliwości zgłoszenia wyjątku po przydzieleniu obiektu, ale przed przypisaniem go do inteligentnego wskaźnika.

Mike Seymour
źródło
2
Zrozumiałem twój pierwszy punkt poprawnie. Czy możesz rozwinąć lub podać linki do drugiego punktu dotyczącego bezpieczeństwa wyjątkowego?
Anup Buchke
22

Jest inny przypadek, w którym dwie możliwości różnią się, oprócz tych już wspomnianych: jeśli chcesz wywołać niepublicznego konstruktora (chronionego lub prywatnego), make_shared może nie być w stanie uzyskać do niego dostępu, podczas gdy wariant z nowymi działa dobrze .

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){}
};
Dr_Sam
źródło
Natrafiłem na ten konkretny problem i zdecydowałem się użyć new, w przeciwnym razie skorzystałbym make_shared. Oto powiązany pytanie o tym: stackoverflow.com/questions/8147027/... .
jigglypuff,
6

Jeśli potrzebujesz specjalnego wyrównania pamięci na obiekcie kontrolowanym przez shared_ptr, nie możesz polegać na make_shared, ale myślę, że to jedyny dobry powód, aby go nie używać.

Simon Ferquel
źródło
2
Drugą sytuacją, w której make_shared jest nieodpowiedni, jest sytuacja, gdy chcesz określić niestandardowy usuwacz.
KnowItAllWannabe
5

Widzę jeden problem ze std :: make_shared, nie obsługuje on konstruktorów prywatnych / chronionych

uderzenie lodu
źródło
3

Shared_ptr: Wykonuje dwie alokacje sterty

  1. Blok kontrolny (liczba referencyjna)
  2. Obiekt zarządzany

Make_shared: Wykonuje tylko jeden przydział sterty

  1. Blok sterujący i dane obiektu.
James
źródło
0

Jeśli chodzi o wydajność i czas poświęcony na alokację, zrobiłem ten prosty test poniżej, stworzyłem wiele instancji na dwa sposoby (jeden na raz):

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

Chodzi o to, że użycie make_shared zajęło dwa razy więcej czasu niż użycie nowego. Tak więc przy użyciu nowego istnieją dwa przydziały sterty zamiast jednego przy użyciu make_shared. Może to głupi test, ale czy nie pokazuje, że używanie make_shared zajmuje więcej czasu niż używanie nowego? Oczywiście mówię tylko o wykorzystanym czasie.

Orlando
źródło
4
Ten test jest nieco bezcelowy. Czy test został przeprowadzony w konfiguracji wersji z optymalizacjami? Również wszystkie twoje przedmioty są natychmiast uwalniane, więc nie jest realistyczne.
Phil1970
0

Myślę, że wyjątek dotyczący bezpieczeństwa w odpowiedzi pana mparka jest nadal ważnym problemem. podczas tworzenia share_ptr w następujący sposób: shared_ptr <T> (nowy T), nowy T może się powieść, a alokacja bloku kontrolnego shared_ptr może się nie powieść. w tym scenariuszu nowo przydzielony T wycieknie, ponieważ shared_ptr nie ma możliwości dowiedzenia się, że został utworzony w miejscu i można go bezpiecznie usunąć. czy coś mi brakuje? Nie wydaje mi się, żeby surowsze zasady oceny parametrów funkcji w jakikolwiek sposób pomogły ...

Martin Vorbrodt
źródło