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.
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.
std::shared_ptr
verwaltet zwei Entitäten:
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 .
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:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
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"));
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.
weak_ptr
s den Steuerblock am Leben?Es muss eine Möglichkeit für weak_ptr
s 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.
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_ptr
s oder weak_ptr
s 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.
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 .
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){}
};
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.
Shared_ptr
: Führt zwei Heap-Zuweisungen durch
Make_shared
: Führt nur eine Heapzuordnung aus
Ü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.
Ich sehe ein Problem mit std :: make_shared, es unterstützt keine privaten/geschützten Konstruktoren