std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));
Viele Google- und Stackoverflow-Beiträge gibt es dazu, aber ich kann nicht verstehen, warum make_shared
ist effizienter als direkt zu verwenden shared_ptr
.
Kann mir jemand die Schritt-für-Schritt-Abfolge der erstellten Objekte und der von beiden durchgeführten Operationen erklären, damit ich verstehen kann, wie make_shared
ist effizient. Ich habe oben ein Beispiel als Referenz gegeben.
Der Unterschied ist das std::make_shared
führt eine Heap-Zuweisung durch, während der Aufruf der std::shared_ptr
Konstruktor führt zwei aus.
Wo finden die Heap-Zuweisungen statt?
std::shared_ptr
verwaltet zwei Einheiten:
- der Steuerblock (speichert Metadaten wie Ref-Counts, Type-Erased Deleter usw.)
- das verwaltete Objekt
std::make_shared
führt eine einzelne Heap-Zuordnung durch, die den Platz berücksichtigt, der sowohl für den Steuerblock als auch für die Daten erforderlich ist. Im anderen Fall new Obj("foo")
ruft eine Heap-Zuweisung für die verwalteten Daten und die auf std::shared_ptr
Der Konstruktor führt einen weiteren für den Steuerblock aus.
Weitere Informationen finden Sie unter Hinweise zur Umsetzung bei cpReferenz.
Update I: Ausnahme-Sicherheit
HINWEIS (2019/08/30)Hinweis: Dies ist seit C++17 kein Problem mehr, da sich die Auswertungsreihenfolge von Funktionsargumenten geändert hat. Insbesondere muss jedes Argument einer Funktion vollständig ausgeführt werden, bevor andere Argumente ausgewertet werden.
Da sich das OP anscheinend über die Ausnahmesicherheitsseite der Dinge 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 Teilausdrücken zulässt, ist eine mögliche Reihenfolge:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>
Nehmen wir nun an, bei Schritt 2 wird eine Ausnahme ausgelöst (z. Rhs
Konstruktor hat eine Ausnahme ausgelöst). Wir verlieren dann den in Schritt 1 zugewiesenen Speicher, da nichts eine Chance hatte, ihn zu bereinigen. Der Kern des Problems hier ist, dass der Raw-Zeiger nicht an die übergeben wurde std::shared_ptr
Konstrukteur sofort.
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 die Verwendung std::make_shared
stattdessen.
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
Update II: Nachteil von std::make_shared
Zitat von Caseys Kommentaren:
Da es nur eine Zuordnung gibt, kann der Speicher des Pointees nicht freigegeben werden, bis der Steuerblock nicht mehr verwendet wird. EIN weak_ptr
kann den Kontrollblock auf unbestimmte Zeit am Leben erhalten.
Warum tun Instanzen von weak_ptr
s den Kontrollblock am Leben erhalten?
Es muss doch einen Weg geben weak_ptr
s, um festzustellen, ob das verwaltete Objekt noch gültig ist (z. B. für lock
). Sie tun dies, indem sie die Anzahl der überprüfen shared_ptr
s, die das verwaltete Objekt besitzen, das im Kontrollblock gespeichert ist. Das Ergebnis ist, dass die Steuerblöcke bis zum lebendig sind shared_ptr
zählen und die weak_ptr
zählen beide Treffer 0.
Zurück zu std::make_shared
Seit std::make_shared
eine einzige Heap-Zuordnung sowohl für den Steuerblock als auch für das verwaltete Objekt vornimmt, 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 Kontrollblock als auch das verwaltete Objekt freigeben können, was passiert, bis es keine gibt shared_ptr
s oder weak_ptr
lebt.
Angenommen, wir haben stattdessen zwei Heap-Zuweisungen für den Steuerblock und das verwaltete Objekt über durchgeführt new
und shared_ptr
Konstrukteur. Dann geben wir den Speicher für das verwaltete Objekt frei (vielleicht früher), wenn es keinen gibt shared_ptr
s am Leben, und geben Sie den Speicher für den Steuerblock (vielleicht später) frei, wenn keine vorhanden sind weak_ptr
lebt.
Es gibt einen weiteren 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){}
};
Der gemeinsame Zeiger 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 zuweisen, um beide zu halten; Das Konstruieren eines gemeinsam genutzten Zeigers aus einem Zeiger auf ein bereits zugewiesenes Objekt muss einen zweiten Block zuweisen, um die Referenzzählung zu speichern.
Neben dieser Effizienz, mit make_shared
bedeutet, dass Sie sich nicht damit befassen müssen new
und rohe Zeiger überhaupt, was eine bessere Ausnahmesicherheit bietet – es gibt keine Möglichkeit, eine Ausnahme auszulösen, nachdem das Objekt zugewiesen wurde, aber bevor es dem intelligenten Zeiger zugewiesen wurde.
Ich sehe ein Problem mit std::make_shared, es unterstützt keine privaten/geschützten Konstruktoren
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, es ist der einzige gute Grund, es nicht zu verwenden.
Shared_ptr
: Führt zwei Heap-Zuweisungen durch
- Steuerblock (Referenzzähler)
- Objekt wird verwaltet
Make_shared
: Führt nur eine Heap-Zuordnung durch
- Kontrollblock- und Objektdaten.
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 >(new T), kann das neue T erfolgreich sein, während die Zuweisung des Steuerblocks von shared_ptr fehlschlagen kann. In diesem Szenario wird das neu zugewiesene T durchsickern, da der shared_ptr nicht wissen kann, dass es direkt erstellt wurde, und es sicher gelöscht werden kann. oder übersehe ich etwas? Ich glaube nicht, dass die strengeren Regeln zur Funktionsparameterbewertung hier in irgendeiner Weise helfen …
Effizienter geht es nicht. Der Grund für die Verwendung ist die Ausnahmesicherheit.
– Yuushi
3. Januar 2014 um 2:50 Uhr
STL behandelte dies in einem seiner Videos auf Kanal 9. Wahrscheinlich war es so Dieses hier.
– Chris
3. Januar 2014 um 2:53 Uhr
@Yuushi: Ausnahmesicherheit ist ein guter Grund, es zu verwenden, aber es ist auch effizienter.
– Mike Seymour
3. Januar 2014 um 2:53 Uhr
Bei 32:15 beginnt er in dem Video, das ich oben verlinkt habe, falls das hilft.
– Chris
3. Januar 2014 um 3:02 Uhr
Vorteil des geringfügigen Codestils: using
make_shared
Du kannst schreibenauto p1(std::make_shared<A>())
und p1 hat den richtigen Typ.– Iwan Vergiljew
11. Januar 2014 um 0:25 Uhr