395 Stimmen

Unterschied zwischen make_shared und normalem shared_ptr in C++

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.

465voto

mpark Punkte 7674

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:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr
  4. 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_ptrs den Steuerungsblock am Leben?

Es muss eine Möglichkeit für weak_ptrs geben festzustellen, ob das verwaltete Objekt immer noch gültig ist (z. B. für lock). Dies geschieht, indem die Anzahl der shared_ptrs ü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_ptrs als auch die Anzahl der weak_ptrs 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_ptrs oder weak_ptrs 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_ptrs mehr existieren, und den Speicher für den Steuerungsblock freigeben (vielleicht später), wenn keine weak_ptrs mehr existieren.

33voto

Dr_Sam Punkte 1798

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

29voto

Mike Seymour Punkte 242473

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.

11voto

icebeat Punkte 131

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

6voto

Simon Ferquel Punkte 366

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X