it-swarm.com.de

std :: shared_ptr als letztes Mittel?

Ich habe gerade die Streams "Going Native 2012" gesehen und die Diskussion über std::shared_ptr Bemerkt. Ich war ein bisschen überrascht, als ich Bjarnes etwas negative Meinung zu std::shared_ptr Und seinen Kommentar hörte, dass es als "letzter Ausweg" verwendet werden sollte, wenn die Lebensdauer eines Objekts ungewiss ist (was ich seiner Meinung nach tun sollte) selten der Fall sein).

Würde es jemandem etwas ausmachen, dies etwas ausführlicher zu erklären? Wie können wir ohne std::shared_ptr Programmieren und trotzdem die Lebensdauer von Objekten auf sichere Weise verwalten?

62
ronag

Wenn Sie eine gemeinsame Eigentümerschaft vermeiden können, ist Ihre Anwendung einfacher und verständlicher und daher weniger anfällig für Fehler, die während der Wartung auftreten. Komplexe oder unklare Eigentumsmodelle führen in der Regel dazu, dass es schwierig ist, Kopplungen verschiedener Teile der Anwendung durch einen gemeinsamen Status zu verfolgen, der möglicherweise nicht leicht zu verfolgen ist.

Vor diesem Hintergrund ist es vorzuziehen, Objekte mit automatischer Speicherdauer zu verwenden und Unterobjekte mit "Wert" zu haben. Andernfalls kann unique_ptr Eine gute Alternative sein, da shared_ptr - wenn nicht das letzte Mittel - auf der Liste der wünschenswerten Tools liegt.

58
CB Bailey

Die Welt, in der Bjarne lebt, ist sehr ... akademisch, aus Mangel an einem besseren Begriff. Wenn Ihr Code so entworfen und strukturiert werden kann, dass Objekte sehr bewusste relationale Hierarchien aufweisen, so dass Eigentumsverhältnisse starr und unnachgiebig sind, fließt der Code in eine Richtung (von High-Level zu Low-Level) und Objekte sprechen nur mit den unteren in die Hierarchie, dann werden Sie nicht viel Bedarf für shared_ptr finden. Es ist etwas, das Sie in den seltenen Fällen verwenden, in denen jemand gegen die Regeln verstoßen muss. Andernfalls können Sie einfach alles in vectors oder andere Datenstrukturen stecken, die Wertesemantik verwenden, und unique_ptr S für Dinge, die Sie einzeln zuweisen müssen.

Das ist zwar eine großartige Welt zum Leben, aber es ist nicht das, was man die ganze Zeit tun muss. Wenn Sie Ihren Code nicht auf diese Weise organisieren können, bedeutet das Design des Systems, das Sie erstellen möchten, dass dies unmöglich (oder nur zutiefst unangenehm) ist ), dann werden Sie feststellen, dass Sie immer mehr das gemeinsame Eigentum an Objekten benötigen.

In einem solchen System ist das Halten nackter Zeiger ... nicht gerade gefährlich, wirft jedoch Fragen auf. Das Tolle an shared_ptr Ist, dass es vernünftige syntaktische Garantien für die Lebensdauer des Objekts bietet. Kann es kaputt gehen? Na sicher. Aber die Leute können auch const_cast Dinge; Die Grundversorgung und Fütterung von shared_ptr sollte eine angemessene Lebensqualität für zugewiesene Objekte gewährleisten, deren Eigentum geteilt werden muss.

Dann gibt es weak_ptr S, die ohne shared_ptr Nicht verwendet werden können. Wenn Ihr System starr strukturiert ist, können Sie einen nackten Zeiger auf ein Objekt speichern, in der Gewissheit, dass die Struktur der Anwendung sicherstellt, dass das Objekt, auf das Sie zeigen, Sie überlebt. Sie können eine Funktion aufrufen, die einen Zeiger auf einen internen oder externen Wert zurückgibt (z. B. ein Objekt mit dem Namen X suchen). In richtig strukturiertem Code steht Ihnen diese Funktion nur zur Verfügung, wenn die Lebensdauer des Objekts garantiert Ihre eigene überschreitet. Daher ist es in Ordnung, diesen nackten Zeiger in Ihrem Objekt zu speichern.

Da diese Steifigkeit in realen Systemen nicht immer erreicht werden kann, müssen Sie einen Weg finden, um die Lebensdauer angemessen sicherzustellen . Manchmal brauchen Sie kein volles Eigentum. Manchmal muss man nur wissen können, wann der Zeiger schlecht oder gut ist. Hier kommt weak_ptr Ins Spiel. Es gab Fälle, in denen ich einen unique_ptr Oder boost::scoped_ptr, aber ich musste einen shared_ptr verwenden, weil ich speziell jemandem einen "flüchtigen" Zeiger geben musste. Ein Zeiger, dessen Lebensdauer unbestimmt war, und der abfragen konnte, wann dieser Zeiger zerstört wurde.

Ein sicherer Weg, um zu überleben, wenn der Zustand der Welt unbestimmt ist.

Könnte dies durch einen Funktionsaufruf geschehen, um den Zeiger zu erhalten, anstatt über weak_ptr? Ja, aber das könnte leichter kaputt gehen. Eine Funktion, die einen nackten Zeiger zurückgibt, kann syntaktisch nicht vorschlagen, dass der Benutzer diesen Zeiger nicht langfristig speichert. Die Rückgabe eines shared_ptr Macht es auch viel zu einfach, ihn einfach zu speichern und möglicherweise die Lebensdauer eines Objekts zu verlängern. Die Rückgabe eines weak_ptr Weist jedoch nachdrücklich darauf hin, dass das Speichern des shared_ptr, Den Sie von lock erhalten, eine ... zweifelhafte Idee ist. Es wird Sie nicht davon abhalten, aber nichts in C++ hindert Sie daran, Code zu brechen. weak_ptr Bietet einen minimalen Widerstand gegen das Natürliche.

Das heißt nicht, dass shared_ptr Nicht überbeansprucht werden kann ; es kann sicherlich. Besonders vor unique_ptr Gab es viele Fälle, in denen ich nur einen boost::shared_ptr Verwendete, weil ich einen RAII-Zeiger herumgeben oder in eine Liste einfügen musste. Ohne Verschiebungssemantik und unique_ptr War boost::shared_ptr Die einzige echte Lösung.

Und Sie können es an Orten verwenden, an denen es völlig unnötig ist. Wie oben erwähnt, kann eine ordnungsgemäße Codestruktur die Notwendigkeit einiger Verwendungen von shared_ptr Beseitigen. Wenn Ihr System jedoch nicht als solches strukturiert werden kann und dennoch das tut, was es benötigt, ist shared_ptr Von erheblichem Nutzen.

48
Nicol Bolas

Ich glaube nicht, dass ich jemals std::shared_ptr Verwendet habe.

Meistens ist ein Objekt einer Sammlung zugeordnet, zu der es während seiner gesamten Lebensdauer gehört. In diesem Fall können Sie einfach whatever_collection<o_type> Oder whatever_collection<std::unique_ptr<o_type>> Verwenden, wobei diese Sammlung Mitglied eines Objekts oder einer automatischen Variablen ist. Wenn Sie keine dynamische Anzahl von Objekten benötigen, können Sie natürlich auch ein automatisches Array mit fester Größe verwenden.

Weder die Iteration durch die Sammlung noch eine andere Operation für das Objekt erfordert eine Hilfsfunktion, um den Besitz zu teilen ... it verwendet das Objekt und kehrt dann zurück, und der Aufrufer garantiert, dass das Objekt für den gesamten Aufruf am Leben bleibt . Dies ist bei weitem der am häufigsten verwendete Vertrag zwischen Anrufer und Angerufenen.


Nicol Bolas kommentierte: "Wenn ein Objekt einen nackten Zeiger festhält und dieses Objekt stirbt ... oops." und "Objekte müssen sicherstellen, dass das Objekt das Leben dieses Objekts durchlebt. Das kann nur shared_ptr."

Ich kaufe dieses Argument nicht. Zumindest nicht, dass shared_ptr Dieses Problem löst. Wie wäre es mit:

  • Wenn eine Hash-Tabelle ein Objekt enthält und sich der Hashcode dieses Objekts ändert ... oops.
  • Wenn eine Funktion einen Vektor iteriert und ein Element in diesen Vektor eingefügt wird ... oops.

Wie bei der Garbage Collection ermutigt die Standardverwendung von shared_ptr Den Programmierer, nicht über den Vertrag zwischen Objekten oder zwischen Funktion und Aufrufer nachzudenken. Es ist erforderlich, über die richtigen Vor- und Nachbedingungen nachzudenken, und die Objektlebensdauer ist nur ein winziges Stück dieses größeren Kuchens.

Objekte "sterben" nicht, ein Teil des Codes zerstört sie. Und shared_ptr Auf das Problem zu werfen, anstatt den Anrufvertrag herauszufinden, ist eine falsche Sicherheit.

39
Ben Voigt

Ich denke lieber nicht absolut (wie "letzter Ausweg"), sondern relativ zur Problemdomäne.

C++ bietet verschiedene Möglichkeiten zur Verwaltung der Lebensdauer. Einige von ihnen versuchen, die Objekte stapelgesteuert neu zu leiten. Einige andere versuchen, dieser Einschränkung zu entgehen. Einige von ihnen sind "wörtlich", andere sind Annäherungen.

Eigentlich können Sie:

  1. reine Wertesemantik verwenden. Funktioniert für relativ kleine Objekte, bei denen "Werte" und nicht "Identitäten" wichtig sind. Dabei können Sie davon ausgehen, dass zwei Person mit demselben name gleich sind . Person (besser: zwei Darstellungen derselben Person ). Die Lebensdauer wird vom Maschinenstapel gewährt, und das Ende spielt für das Programm keine Rolle (da es sich bei einer Person um den Namen handelt ), egal was Person es trägt)
  2. Stapel zugewiesene Objekte verwenden und zugehörige Referenzen oder Zeiger: Ermöglicht Polymorphismus und gewährt Objektlebensdauer. Keine Notwendigkeit für "intelligente Zeiger", da Sie sicherstellen, dass kein Objekt von Strukturen "gezeigt" werden kann, die länger im Stapel verbleiben als das Objekt, auf das sie zeigen (erstellen Sie zuerst das Objekt, dann die Strukturen, die darauf verweisen).
  3. Verwenden Sie stapelverwaltete Heap-zugewiesene Objekte: Dies ist, was std :: vector und alle Container tun, und wat std::unique_ptr (Sie können es sich als Vektor mit Größe 1 vorstellen). Auch hier geben Sie zu, dass das Objekt vor (nach) der Datenstruktur, auf die es sich bezieht, zu existieren beginnt (und endet).

Die Schwäche dieser Methoden besteht darin, dass Objekttypen und -mengen während der Ausführung von Aufrufen auf tieferer Stapelebene nicht variieren können, je nachdem, wo sie erstellt werden. Alle diese Techniken "scheitern" an ihrer Stärke in allen Situationen, in denen das Erstellen und Löschen von Objekten eine Folge von Benutzeraktivitäten ist, so dass der Laufzeittyp des Objekts nicht zur Kompilierungszeit bekannt ist und es zu Überstrukturen kommen kann, die sich auf Objekte beziehen Der Benutzer fordert Sie auf, einen Funktionsaufruf auf Stack-Ebene zu entfernen. In diesen Fällen müssen Sie entweder:

  • einführung einer gewissen Disziplin in Bezug auf die Verwaltung von Objekten und verwandten Referenzstrukturen oder ...
  • gehen Sie irgendwie auf die dunkle Seite von "Escape the Pure Stack Based Lifetime": Das Objekt muss unabhängig von den Funktionen, die es erstellt haben, verlassen. Und muss gehen ... bis sie gebraucht werden .

C++ isteslf hat keinen nativen Mechanismus zum Überwachen dieses Ereignisses (while(are_they_needed)), daher müssen Sie sich annähern mit:

  1. Shared Ownership verwenden: Objekte, deren Leben an einen "Referenzzähler" gebunden ist: Funktioniert, wenn "Ownership" hierarchisch organisiert werden kann, schlägt fehl, wenn Besitzschleifen vorhanden sein können. Dies ist, was std :: shared_ptr tut. Und schwache_ptr kann verwendet werden, um die Schleife zu brechen. Dies funktioniert die meiste Zeit, scheitert jedoch bei großen Designs, bei denen viele Designer in verschiedenen Teams arbeiten und es keinen klaren Grund gibt (etwas, das von einer gewissen Anforderung herrührt), wer was muffig besitzt (das typische Beispiel sind Ketten mit zwei Vorlieben: ist das Vorherige aufgrund der nächsten Bezugnahme auf die vorherige oder die nächste Eigentümerin der vorherigen Bezugnahme auf die nächste? In Abwesenheit einer Anforderung sind die beiden Lösungen gleichwertig, und bei großen Projekten besteht das Risiko, dass Sie sie verwechseln.
  2. Verwenden Sie einen Müllsammelhaufen: Die Lebensdauer ist Ihnen einfach egal. Sie führen den Sammler von Zeit zu Zeit aus und was nicht erreichbar ist, wird als "nicht mehr benötigt" angesehen und ... nun ... ähm ... zerstört? abgeschlossen? gefroren?. Es gibt eine Reihe von GC-Sammlern, aber ich finde nie einen, der C++ wirklich kennt. Die meisten von ihnen haben freien Speicher, ohne sich um die Zerstörung von Objekten zu kümmern.
  3. Verwenden Sie einen C++ - fähigen Garbage Collector mit einer geeigneten Standardmethodenschnittstelle. Viel Glück, es zu finden.

Wenn Sie zur allerersten bis zur letzten Lösung übergehen, erhöht sich die Menge der zusätzlichen Datenstruktur, die zum Verwalten der Objektlebensdauer erforderlich ist, mit dem Zeitaufwand für deren Organisation und Wartung.

Garbage Collector hat Kosten, shared_ptr weniger, unique_ptr noch weniger und stapelverwaltete Objekte nur sehr wenige.

Ist shared_ptr Der "letzte Ausweg"?. Nein, das ist es nicht: Der letzte Ausweg sind Müllsammler. shared_ptr Ist eigentlich der std:: Vorgeschlagene letzte Ausweg. Aber vielleicht ist die richtige Lösung, wenn Sie in der Situation sind, die ich erklärt habe.

16

Das einzige, was Herb Sutter in einer späteren Sitzung erwähnt hat, ist, dass jedes Mal, wenn Sie einen shared_ptr<> Kopieren, ein ineinandergreifendes Inkrement/Dekrement auftreten muss. Bei Multithread-Code auf einem Mehrkernsystem ist die Speichersynchronisation nicht unerheblich. Wenn Sie die Wahl haben, ist es besser, entweder einen Stapelwert oder einen unique_ptr<> Zu verwenden und Referenzen oder Rohzeiger weiterzugeben.

9
Eclipse

Ich erinnere mich nicht, ob das letzte "Mittel" genau das Wort war, das er verwendet hat, aber ich glaube, dass die tatsächliche Bedeutung dessen, was er sagte, die letzte "Wahl" war: angesichts klarer Eigentumsbedingungen; unique_ptr, schwach_ptr, shared_ptr und sogar nackte Zeiger haben ihren Platz.

Alle waren sich einig, dass wir (Entwickler, Buchautoren usw.) alle in der "Lernphase" von C++ 11 sind und Muster und Stile definiert werden.

Als Beispiel erklärte Herb, wir sollten neue Ausgaben einiger der wegweisenden C++ - Bücher erwarten, wie Effective C++ (Meyers) und C++ Coding Standards (Sutter & Alexandrescu), ein paar Jahre später, während die Erfahrung und Best Practices der Branche mit C++ 11 nachlassen.

7
Eddie Velasquez

Ich denke, was er damit meint, ist, dass es für jeden üblich wird, shared_ptr zu schreiben, wenn er einen Standardzeiger geschrieben hat (wie eine Art globaler Ersatz), und dass er als Copout verwendet wird, anstatt ihn tatsächlich zu entwerfen oder zumindest Planung für die Objekterstellung und -löschung.

Das andere, was die Leute vergessen (neben dem im obigen Material erwähnten Engpass beim Sperren/Aktualisieren/Entsperren), ist, dass shared_ptr allein keine Zyklusprobleme löst. Mit shared_ptr können Sie weiterhin Ressourcen verlieren:

Objekt A enthält einen gemeinsamen Zeiger auf ein anderes Objekt. A Objekt B erstellt A a1 und A a2 und weist a1.otherA = a2 zu. und a2.otherA = a1; Die gemeinsam genutzten Zeiger von Objekt B, mit denen a1, a2 erstellt wurden, verlassen den Gültigkeitsbereich (z. B. am Ende einer Funktion). Jetzt haben Sie ein Leck - niemand anderes bezieht sich auf a1 und a2, aber sie beziehen sich aufeinander, sodass ihre Ref-Zählungen immer 1 sind und Sie durchgesickert sind.

Dies ist das einfache Beispiel: Wenn dies in echtem Code geschieht, geschieht dies normalerweise auf komplizierte Weise. Es gibt eine Lösung mit schwachem_ptr, aber so viele Leute machen jetzt einfach überall shared_ptr und kennen nicht einmal das Leckproblem oder sogar schwach_ptr.

Um es zusammenzufassen: Ich denke, die Kommentare, auf die das OP verweist, laufen darauf hinaus:

nabhängig davon, in welcher Sprache Sie arbeiten (verwaltet, nicht verwaltet oder etwas dazwischen mit Referenzzählungen wie shared_ptr), müssen Sie die Objekterstellung, Lebensdauer und Zerstörung verstehen und absichtlich entscheiden.

edit: auch wenn das bedeutet "unbekannt, ich muss ein shared_ptr verwenden", hast du immer noch darüber nachgedacht und tust dies absichtlich.

5
anon

Ich werde aus meiner Erfahrung mit Objective-C antworten, einer Sprache, in der alle Objekte auf dem Heap gezählt und zugeordnet werden. Aufgrund der Möglichkeit, Objekte auf eine Weise zu behandeln, sind die Dinge für den Programmierer viel einfacher. Dadurch konnten Standardregeln definiert werden, die bei Einhaltung die Robustheit des Codes und keine Speicherlecks gewährleisten. Es ermöglichte auch clevere Compiler-Optimierungen wie die jüngste ARC (automatische Referenzzählung).

Mein Punkt ist, dass shared_ptr eher Ihre erste Option als der letzte Ausweg sein sollte. Verwenden Sie die Referenzzählung standardmäßig und andere Optionen nur, wenn Sie sicher sind, was Sie tun. Sie sind produktiver und Ihr Code ist robuster.

3
Dimitris

Ich werde versuchen, die Frage zu beantworten:

Wie können wir ohne std :: shared_ptr programmieren und trotzdem die Objektlebensdauer auf sichere Weise verwalten?

C++ bietet eine Vielzahl verschiedener Möglichkeiten, Speicher zu erstellen, zum Beispiel:

  1. Verwenden Sie im Klassenbereich struct A { MyStruct s1,s2; }; Anstelle von shared_ptr. Dies gilt nur für fortgeschrittene Programmierer, da Sie wissen müssen, wie Abhängigkeiten funktionieren, und die Abhängigkeit so steuern können, dass sie auf einen Baum beschränkt ist. Die Reihenfolge der Klassen in der Header-Datei ist dabei ein wichtiger Aspekt. Es scheint, dass diese Verwendung bereits bei nativen integrierten C++ - Typen üblich ist, aber die Verwendung mit vom Programmierer definierten Klassen scheint aufgrund dieser Abhängigkeits- und Reihenfolge der Klassenprobleme weniger häufig zu sein. Diese Lösung hat auch Probleme mit sizeof. Programmierer sehen Probleme darin als eine Anforderung, Vorwärtsdeklarationen oder unnötige #includes zu verwenden, und daher werden viele Programmierer auf eine minderwertige Lösung von Zeigern und später auf shared_ptr zurückgreifen.
  2. Verwenden Sie MyClass &find_obj(int i); + clone () anstelle von shared_ptr<MyClass> create_obj(int i);. Viele Programmierer möchten Fabriken zum Erstellen neuer Objekte erstellen. shared_ptr ist ideal für diese Art der Verwendung geeignet. Das Problem besteht darin, dass bereits eine komplexe Speicherverwaltungslösung mit Heap/Free Store-Zuordnung anstelle einer einfacheren stapel- oder objektbasierten Lösung angenommen wird. Eine gute C++ - Klassenhierarchie unterstützt alle Speicherverwaltungsschemata, nicht nur eines davon. Die referenzbasierte Lösung kann funktionieren, wenn das zurückgegebene Objekt im enthaltenen Objekt gespeichert ist, anstatt die lokale Funktionsbereichsvariable zu verwenden. Die Übertragung des Eigentums von der Fabrik auf den Benutzercode sollte vermieden werden. Das Kopieren des Objekts nach Verwendung von find_obj () ist eine gute Möglichkeit, damit umzugehen. Normale Kopierkonstruktoren und normale Konstruktoren (verschiedener Klassen) mit dem Auffrischungsparameter oder clone () für polymorphe Objekte können damit umgehen.
  3. Verwendung von Referenzen anstelle von Zeigern oder shared_ptrs. Jede c ++ - Klasse verfügt über Konstruktoren, und jedes Referenzdatenelement muss initialisiert werden. Diese Verwendung kann viele Verwendungen von Zeigern und shared_ptrs vermeiden. Sie müssen nur auswählen, ob sich Ihr Speicher innerhalb oder außerhalb des Objekts befindet, und die Strukturlösung oder Referenzlösung basierend auf der Entscheidung auswählen. Probleme mit dieser Lösung hängen normalerweise mit der Vermeidung von Konstruktorparametern zusammen, was häufig vorkommt, aber problematisch ist, und mit dem Missverständnis, wie Schnittstellen für Klassen entworfen werden sollten.
1
tp1