Unterschied zwischen make_shared und normal shared_ptr in C ++

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

Es gibt viele Google- und Stackoverflow-Posts, aber ich kann nicht verstehen, warum dies make_sharedeffizienter ist als die direkte Verwendung shared_ptr.

Kann mir jemand Schritt für Schritt erklären, wie Objekte erstellt und von beiden ausgeführt werden, damit ich verstehen kann, wie make_sharedeffizient sie sind? Ich habe oben ein Beispiel als Referenz gegeben.

Anup Buchke
quelle
4
Es ist nicht effizienter. Der Grund für die Verwendung ist die Ausnahmesicherheit.
Yuushi
Einige Artikel sagen, es vermeidet einige Baukosten. Können Sie bitte mehr dazu erklären?
Anup Buchke
16
@ Yuushi: Ausnahmesicherheit ist ein guter Grund, es zu verwenden, aber es ist auch effizienter.
Mike Seymour
3
32:15 beginnt er in dem Video, auf das ich oben verlinkt habe, wenn das hilft.
Chris
4
Kleiner Vorteil im Codestil : Wenn make_sharedSie schreiben können, hat auto p1(std::make_shared<A>())p1 den richtigen Typ.
Ivan Vergiliev

Antworten:

332

Der Unterschied besteht darin, dass std::make_sharedeine Heap-Zuweisung ausgeführt wird, während beim Aufrufen des std::shared_ptrKonstruktors zwei ausgeführt werden.

Wo finden die Heap-Zuweisungen statt?

std::shared_ptr verwaltet zwei Entitäten:

  • der Steuerblock (speichert Metadaten wie Ref-Count, Typ-gelöschter Deleter usw.)
  • das zu verwaltende Objekt

std::make_sharedführt eine einzelne Heap-Zuordnung durch, die den für den Steuerblock und die Daten erforderlichen Speicherplatz berücksichtigt. Im anderen Fall wird new Obj("foo")eine Heap-Zuordnung für die verwalteten Daten aufgerufen, und der std::shared_ptrKonstruktor führt eine weitere für den Steuerblock aus.

Weitere Informationen finden Sie in den Implementierungshinweisen unter cppreference .

Update I: Ausnahmesicherheit

HINWEIS (30.08.2019) : Dies ist seit C ++ 17 kein Problem, da sich die Auswertungsreihenfolge der Funktionsargumente geändert hat. Insbesondere muss jedes Argument für eine Funktion vollständig ausgeführt werden, bevor andere Argumente ausgewertet werden.

Da sich das OP anscheinend über die Ausnahmesicherheit wundert, habe ich meine Antwort aktualisiert.

Betrachten Sie dieses Beispiel,

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

Da C ++ eine beliebige Reihenfolge der Auswertung von Unterausdrücken ermöglicht, ist eine mögliche Reihenfolge:

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

Angenommen, in Schritt 2 wird eine Ausnahme ausgelöst (z. B. hat der RhsKonstruktor eine Ausnahme wegen Speichermangel ausgelöst ). Wir verlieren dann den in Schritt 1 zugewiesenen Speicher, da nichts die Möglichkeit gehabt hat, ihn zu bereinigen. Der Kern des Problems besteht darin, dass der Rohzeiger nicht sofort an den std::shared_ptrKonstruktor übergeben wurde.

Eine Möglichkeit, dies zu beheben, besteht darin, sie in separaten Zeilen auszuführen, damit diese willkürliche Reihenfolge nicht auftreten kann.

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

Der bevorzugte Weg, dies zu lösen, ist natürlich, std::make_sharedstattdessen zu verwenden .

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

Update II: Nachteil von std::make_shared

Zitiert Caseys Kommentare:

Da es nur eine Zuordnung gibt, kann der Speicher des Pointees erst freigegeben werden, wenn der Steuerblock nicht mehr verwendet wird. A weak_ptrkann den Steuerblock unbegrenzt am Leben halten.

Warum halten Instanzen von weak_ptrs den Steuerblock am Leben?

Es muss eine Möglichkeit für weak_ptrs geben, festzustellen, ob das verwaltete Objekt noch gültig ist (z. B. für lock). Dazu überprüfen sie die Anzahl der shared_ptrs, denen das verwaltete Objekt gehört, das im Steuerblock gespeichert ist. Das Ergebnis ist, dass die Steuerblöcke so lange aktiv sind, bis sowohl die shared_ptrZählung als auch die weak_ptrZählung 0 erreichen.

Zurück zu std::make_shared

Da std::make_sharedsowohl für den Steuerblock als auch für das verwaltete Objekt eine einzige Heap-Zuordnung vorgenommen wird, gibt es keine Möglichkeit, den Speicher für den Steuerblock und das verwaltete Objekt unabhängig voneinander freizugeben. Wir müssen warten, bis wir sowohl den Steuerblock als auch das verwaltete Objekt freigeben können. Dies geschieht, bis keine shared_ptrs oder weak_ptrs mehr lebendig sind.

Angenommen, wir haben stattdessen zwei Heap-Zuordnungen für den Steuerblock und das verwaltete Objekt über newund shared_ptrKonstruktor durchgeführt. Dann geben wir den Speicher für das verwaltete Objekt frei (möglicherweise früher), wenn kein shared_ptrs aktiv ist, und geben den Speicher für den Steuerblock frei (möglicherweise später), wenn kein weak_ptrs aktiv ist.

mpark
quelle
53
Es ist eine gute Idee, auch den kleinen Nachteil des make_sharedEckfalls zu erwähnen : Da es nur eine Zuordnung gibt, kann der Speicher des Pointees erst freigegeben werden, wenn der Steuerblock nicht mehr verwendet wird. A weak_ptrkann den Steuerblock unbegrenzt am Leben halten.
Casey
13
Ein anderer, stilistisch, Punkt ist: Wenn Sie make_sharedund make_uniquekonsequent, werden Sie nicht roh Zeiger haben besitzen eine kann jedes Vorkommen behandeln newals Code Geruch.
Philipp
5
Wenn es nur ein shared_ptrund kein weak_ptrs gibt, wird durch Aufrufen reset()der shared_ptrInstanz der Steuerblock gelöscht. Dies ist aber unabhängig davon oder ob make_sharedverwendet wurde. Die Verwendung make_sharedmacht einen Unterschied, da sie die Lebensdauer des für das verwaltete Objekt zugewiesenen Speichers verlängern kann . Wenn die shared_ptrAnzahl 0 trifft, wird der Destruktor für das verwaltete Objekt aufgerufen , unabhängig davon make_shared, aber befreiend nur sein Gedächtnis, wenn geschehen make_sharedwurde nicht verwendet. Hoffe das macht es klarer.
Park
4
Erwähnenswert ist auch, dass make_shared die Optimierung "Wir wissen, wo Sie leben" nutzen kann, mit der der Steuerblock ein kleinerer Zeiger sein kann. (Einzelheiten finden Sie in der GN2012-Präsentation von Stephan T. Lavavej in ungefähr Minute 12.) make_shared vermeidet somit nicht nur eine Zuordnung, sondern weist auch weniger Gesamtspeicher zu.
KnowItAllWannabe
1
@HannaKhalil: Ist das vielleicht das Reich dessen, wonach du suchst ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

Der gemeinsam genutzte Zeiger verwaltet sowohl das Objekt selbst als auch ein kleines Objekt, das den Referenzzähler und andere Verwaltungsdaten enthält. make_sharedkann einen einzelnen Speicherblock zuweisen, um beide zu speichern; Wenn Sie einen gemeinsam genutzten Zeiger aus einem Zeiger auf ein bereits zugewiesenes Objekt erstellen, muss ein zweiter Block zum Speichern des Referenzzählers zugewiesen werden.

Neben dieser Effizienz make_sharedbedeutet die Verwendung , dass Sie sich überhaupt nicht mit newund rohen Zeigern befassen müssen , was eine bessere Ausnahmesicherheit bietet. Es besteht keine Möglichkeit, eine Ausnahme nach dem Zuweisen des Objekts, aber vor dem Zuweisen zum intelligenten Zeiger auszulösen.

Mike Seymour
quelle
2
Ich habe Ihren ersten Punkt richtig verstanden. Können Sie bitte den zweiten Punkt zur Ausnahmesicherheit näher erläutern oder einige Links geben?
Anup Buchke
22

Es gibt einen anderen Fall, in dem sich die beiden Möglichkeiten zusätzlich zu den bereits erwähnten unterscheiden: Wenn Sie einen nicht öffentlichen Konstruktor (geschützt oder privat) aufrufen müssen, kann make_shared möglicherweise nicht darauf zugreifen, während die Variante mit dem neuen problemlos funktioniert .

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
quelle
Ich bin genau auf dieses Problem gestoßen und habe newmich für die Verwendung entschieden , sonst hätte ich es verwendet make_shared. Hier ist eine verwandte Frage dazu: stackoverflow.com/questions/8147027/… .
Jigglypuff
6

Wenn Sie eine spezielle Speicherausrichtung für das von shared_ptr gesteuerte Objekt benötigen, können Sie sich nicht auf make_shared verlassen, aber ich denke, dies ist der einzige gute Grund, es nicht zu verwenden.

Simon Ferquel
quelle
2
Eine zweite Situation, in der make_shared unangemessen ist, besteht darin, dass Sie einen benutzerdefinierten Löscher angeben möchten.
KnowItAllWannabe
5

Ich sehe ein Problem mit std :: make_shared, es unterstützt keine privaten / geschützten Konstruktoren

Eisschlag
quelle
3

Shared_ptr: Führt zwei Heap-Zuordnungen durch

  1. Steuerblock (Referenzanzahl)
  2. Objekt verwaltet

Make_shared: Führt nur eine Heap-Zuordnung durch

  1. Steuerblock- und Objektdaten.
James
quelle
0

In Bezug auf die Effizienz und die Zeit, die für die Zuweisung aufgewendet wurde, habe ich diesen einfachen Test unten durchgeführt. Ich habe viele Instanzen auf zwei Arten (nacheinander) erstellt:

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

Die Sache ist, dass die Verwendung von make_shared im Vergleich zur Verwendung von new die doppelte Zeit in Anspruch nahm. Bei Verwendung von new gibt es also zwei Heap-Zuweisungen anstelle einer mit make_shared. Vielleicht ist dies ein dummer Test, aber zeigt er nicht, dass die Verwendung von make_shared mehr Zeit in Anspruch nimmt als die Verwendung von new? Natürlich spreche ich nur von der verwendeten Zeit.

Orlando
quelle
4
Dieser Test ist etwas sinnlos. Wurde der Test in der Release-Konfiguration mit optimierten Optimierungen durchgeführt? Außerdem werden alle Ihre Artikel sofort freigegeben, sodass dies nicht realistisch ist.
Phil1970
0

Ich denke, der Teil der Ausnahmesicherheit in der Antwort von Herrn mpark ist immer noch ein berechtigtes Anliegen. Wenn Sie einen shared_ptr wie folgt erstellen: shared_ptr <T> (neues T), kann das neue T erfolgreich sein, während die Zuweisung des Steuerblocks durch shared_ptr möglicherweise fehlschlägt. In diesem Szenario leckt das neu zugewiesene T, da das shared_ptr nicht wissen kann, dass es direkt erstellt wurde, und es sicher gelöscht werden kann. oder fehlt mir etwas Ich denke nicht, dass die strengeren Regeln für die Auswertung von Funktionsparametern hier in irgendeiner Weise helfen ...

Martin Vorbrodt
quelle