wake-up-neo.com

Unterschied in make_shared und normalem shared_ptr in C++

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

Es gibt viele Google- und Stackoverflow-Einträge, aber ich kann nicht verstehen, warum make_shared effizienter ist als direkt mit shared_ptr

Kann mir jemand Schritt für Schritt die Reihenfolge der von beiden erstellten Objekte und durchgeführten Operationen erklären, damit ich verstehen kann, wie make_shared effizient ist. Ich habe oben ein Beispiel als Referenz gegeben.

214
Anup Buchke

Der Unterschied besteht darin, dass std::make_shared eine Heap-Zuweisung durchführt, während der Aufruf des std::shared_ptr -Konstruktors zwei durchführt.

Wo finden die Heap-Allokationen statt?

std::shared_ptr verwaltet zwei Entitäten:

  • der Steuerblock (speichert Metadaten wie Refcounts, typgelöschte Deleter usw.)
  • das Objekt, das verwaltet wird

std::make_shared führt eine einzelne Heap-Zuweisung durch, die den für den Steuerblock und die Daten erforderlichen Speicherplatz berücksichtigt. Im anderen Fall ruft new Obj("foo") eine Heap-Zuordnung für die verwalteten Daten auf, und der Konstruktor std::shared_ptr führt eine andere 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 über die Ausnahmesicherheitsaspekte zu wundern scheint, 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 für die Auswertung von Unterausdrücken zulässt, ist eine mögliche Reihenfolge:

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

Angenommen, wir erhalten in Schritt 2 eine Ausnahmebedingung (z. B. die Ausnahmebedingung "Nicht genügend Speicher", der Konstruktor "Rhs" hat eine Ausnahmebedingung 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 rohe Zeiger nicht sofort an den Konstruktor std::shared_ptr übergeben wurde.

Eine Möglichkeit, dies zu beheben, besteht darin, sie in separaten Zeilen auszuführen, sodass 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, stattdessen std::make_shared zu verwenden.

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

Update II: Nachteil von std::make_shared

Zitat von Casey :

Da es nur eine Zuordnung gibt, kann der Speicher des Empfängers nicht freigegeben werden, bis der Steuerblock nicht mehr verwendet wird. Ein weak_ptr kann den Steuerblock unbegrenzt am Leben erhalten.

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). Sie tun dies, indem sie die Anzahl der shared_ptr prüfen, die das verwaltete Objekt besitzen, das im Steuerblock gespeichert ist. Das Ergebnis ist, dass die Steuerblöcke aktiv sind, bis die Anzahl shared_ptr und die Anzahl weak_ptr beide 0 ergeben.

Zurück zu std::make_shared

Da std::make_shared sowohl für den Steuerblock als auch für das verwaltete Objekt eine einzelne Heap-Zuordnung vornimmt, besteht 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 am Leben sind.

Angenommen, wir haben stattdessen zwei Heap-Zuweisungen für den Steuerblock und das verwaltete Objekt über den Konstruktor new und shared_ptr durchgeführt. Dann geben wir den Speicher für das verwaltete Objekt (möglicherweise früher) frei, wenn kein shared_ptr vorhanden ist, und geben den Speicher für den Steuerblock (möglicherweise später) frei, wenn kein weak_ptr vorhanden ist.

300
mpark

Der gemeinsam genutzte Zeiger verwaltet sowohl das Objekt selbst als auch ein kleines Objekt, das die Referenzanzahl und andere Housekeeping-Daten enthält. make_shared kann einen einzelnen Speicherblock zuordnen, um beide zu speichern. Um einen gemeinsam genutzten Zeiger von einem Zeiger auf ein bereits zugewiesenes Objekt zu erstellen, muss ein zweiter Block zum Speichern des Referenzzählwerts zugewiesen werden.

Neben dieser Effizienz bedeutet die Verwendung von make_shared, dass Sie sich nicht mit new und rohen Zeigern befassen müssen, was zu einer besseren Ausnahmesicherheit führt. Es gibt keine Möglichkeit, eine Ausnahme auszulösen, nachdem das Objekt zugewiesen wurde, aber bevor es dem intelligenten Zeiger zugewiesen wird .

19
Mike Seymour

Es gibt noch einen anderen Fall, bei dem sich die beiden Möglichkeiten zusätzlich zu den bereits genannten 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 System gut 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){}
};
13
Dr_Sam

Wenn Sie ein spezielles Memory-Alignment für das von shared_ptr kontrollierte Objekt benötigen, können Sie sich nicht auf make_shared verlassen, aber ich denke, es ist der einzige gute Grund, es nicht zu verwenden.

3
Simon Ferquel

Shared_ptr: Führt zwei Heap-Zuweisungen durch

  1. Steuerblock (Referenzzähler)
  2. Objekt wird verwaltet

Make_shared: Führt nur eine Heapzuordnung aus

  1. Steuerblock- und Objektdaten.
2
James

Über die Effizienz und die für die Zuteilung aufgewendete Zeit habe ich diesen einfachen Test gemacht. Ich habe viele Instanzen auf zwei Arten erstellt (eine nach der anderen):

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 mit make_shared doppelt so viel Zeit gebraucht wurde wie mit new. Bei der Verwendung von new gibt es also zwei Heap-Zuordnungen, statt einer mit make_shared. Vielleicht ist das ein dummer Test, aber zeigt das nicht, dass die Verwendung von make_shared länger dauert als die Verwendung von new? Ich spreche natürlich nur von Zeit.

0
orlando

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

0
icebeat