std::shared_ptr p1 = std::make_shared("foo");
std::shared_ptr p2(new Object("foo"));
Viele Google- und Stackoverflow-Beiträge gibt es dazu, aber ich verstehe nicht, warum make_shared effizienter ist als die direkte Verwendung von shared_ptr.
Kann mir jemand Schritt-für-Schritt erklären, welche Objekte erstellt werden und welche Operationen von beiden durchgeführt werden, damit ich verstehen kann, wie make_shared effizient ist? Ich habe ein Beispiel oben als Referenz gegeben.
Antworten
Zu viele Anzeigen?Der Unterschied besteht darin, dass std::make_shared
eine Heap-Allokation durchführt, während der Aufruf des std::shared_ptr
-Konstruktors zwei Allokationen durchführt.
Wo finden die Heap-Allokationen statt?
std::shared_ptr
verwaltet zwei Entitäten:
- den Steuerungsblock (speichert Metadaten wie Referenzzähler, Typ-erased Deleter usw.)
- das verwaltete Objekt
std::make_shared
führt eine einzige Heap-Allokation durch, die den für sowohl den Steuerungsblock als auch die Daten erforderlichen Speicher berücksichtigt. Im anderen Fall ruft new Obj("foo")
eine Heap-Allokation für die verwalteten Daten auf und der std::shared_ptr
-Konstruktor führt eine weitere für den Steuerungsblock durch.
Weitere Informationen finden Sie in den Implementierungshinweisen unter cppreference.
Update I: Exception-Sicherheit
HINWEIS (2019/08/30): Dies ist seit C++17 kein Problem mehr, aufgrund der Änderungen in der Auswertungsreihenfolge der Funktionsargumente. Jedes Argument einer Funktion muss vollständig ausgeführt werden, bevor andere Argumente ausgewertet werden.
Da sich der OP anscheinend Gedanken über die Seiten der Exception-Sicherheit gemacht hat, habe ich meine Antwort aktualisiert.
Betrachten Sie dieses Beispiel:
void F(const std::shared_ptr &lhs, const std::shared_ptr &rhs) { /* ... */ }
F(std::shared_ptr(new Lhs("foo")),
std::shared_ptr(new Rhs("bar")));
Da C++ eine beliebige Auswertungsreihenfolge von Subausdrücken zulässt, ist eine mögliche Reihenfolge:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr
std::shared_ptr
Angenommen, wir bekommen bei Schritt 2 eine Ausnahme geworfen (z.B. Speicherausnahme, Rhs
-Konstruktor wirft eine Ausnahme). Dann verlieren wir den im Schritt 1 allokierten Speicher, da nichts die Chance hatte, ihn aufzuräumen. Der Kern des Problems hier ist, dass der Raw-Pointer nicht sofort an den std::shared_ptr
-Konstruktor übergeben wurde.
auto lhs = std::shared_ptr(new Lhs("foo"));
auto rhs = std::shared_ptr(new Rhs("bar"));
F(lhs, rhs);
Der bevorzugte Weg, dies zu lösen, besteht natürlich darin, stattdessen std::make_shared
zu verwenden.
F(std::make_shared("foo"), std::make_shared("bar"));
Update II: Nachteil von std::make_shared
Unter Bezugnahme auf die Kommentare von Casey:
Da es nur eine Allokation gibt, kann der Speicher des Pointees nicht deallokiert werden, bis der Steuerungsblock nicht mehr verwendet wird. Ein
weak_ptr
kann den Steuerungsblock auf unbestimmte Zeit am Leben halten.
Warum halten Instanzen von weak_ptr
s den Steuerungsblock am Leben?
Es muss eine Möglichkeit für weak_ptr
s geben festzustellen, ob das verwaltete Objekt immer noch gültig ist (z. B. für lock
). Dies geschieht, indem die Anzahl der shared_ptr
s überprüft wird, die das verwaltete Objekt besitzen und im Steuerungsblock gespeichert sind. Das Ergebnis ist, dass die Steuerungsblöcke am Leben bleiben, bis sowohl die Anzahl der shared_ptr
s als auch die Anzahl der weak_ptr
s 0 erreichen.
Zurück zu std::make_shared
Da std::make_shared
eine einzige Heap-Allokation für sowohl den Steuerungsblock als auch das verwaltete Objekt durchführt, gibt es keine Möglichkeit, den Speicher für den Steuerungsblock und das verwaltete Objekt unabhängig voneinander freizugeben. Wir müssen warten, bis wir sowohl den Steuerungsblock als auch das verwaltete Objekt freigeben können, was erst dann geschieht, wenn keine shared_ptr
s oder weak_ptr
s mehr existieren.
Angenommen, wir hätten stattdessen zwei Heap-Allokationen für den Steuerungsblock und das verwaltete Objekt über new
und den shared_ptr
-Konstruktor durchgeführt. Dann würden wir den Speicher für das verwaltete Objekt freigeben (vielleicht früher), wenn keine shared_ptr
s mehr existieren, und den Speicher für den Steuerungsblock freigeben (vielleicht später), wenn keine weak_ptr
s mehr existieren.
Es gibt einen weiteren Fall, in dem sich die beiden Möglichkeiten unterscheiden, zusätzlich zu denen, die bereits erwähnt wurden: 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 einwandfrei funktioniert.
class A
{
public:
A(): val(0){}
std::shared_ptr createNext(){ return std::make_shared(val+1); }
// Ungültig, da make_shared intern A(int) aufrufen muss
std::shared_ptr createNext(){ return std::shared_ptr(new A(val+1)); }
// Funktioniert einwandfrei, weil A(int) explizit aufgerufen wird
private:
int val;
A(int v): val(v){}
};
Der shared pointer verwaltet sowohl das Objekt selbst als auch ein kleines Objekt, das die Referenzzählung und andere Verwaltungsdaten enthält. make_shared
kann einen einzigen Speicherblock zuteilen, um beide zu halten; das Konstruieren eines shared pointers aus einem Zeiger auf ein bereits zugewiesenes Objekt erfordert das Zuteilen eines zweiten Blocks zur Aufbewahrung der Referenzzählung.
Neben dieser Effizienz bedeutet die Verwendung von make_shared
, dass Sie gar nicht mit new
und Raw-Zeigern umgehen müssen, was eine bessere Ausnahme-Sicherheit bietet - es besteht keine Möglichkeit, eine Ausnahme zu werfen, nachdem das Objekt zugewiesen, aber bevor es dem Smart Pointer zugewiesen wurde.
Ich sehe ein Problem mit std::make_shared, es unterstützt keine privaten/geschützten Konstruktoren
std::shared_ptr(new T(args...)) kann einen nicht öffentlichen Konstruktor von T aufrufen, wenn es im Kontext ausgeführt wird, in dem er zugänglich ist, während std::make_shared öffentlichen Zugriff auf den ausgewählten Konstruktor erfordert.
https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared#Notes
- See previous answers
- Weitere Antworten anzeigen