std :: shared_ptr als letzter Ausweg?

59

Ich habe gerade die "Going Native 2012" -Streams angesehen und die Diskussion darüber bemerkt std::shared_ptr. Ich war ein bisschen überrascht, als ich Bjarnes etwas negative Meinung std::shared_ptrund seine Bemerkung hörte, dass es als "letzter Ausweg" verwendet werden sollte, wenn die Lebensdauer eines Objekts ungewiss ist (was seiner Meinung nach selten der Fall sein sollte).

Würde es jemand interessieren, dies etwas ausführlicher zu erklären? Wie können wir programmieren, ohne std::shared_ptrdie Lebensdauer von Objekten sicher zu verwalten?

Ronag
quelle
8
Verwenden Sie keine Zeiger? Haben Sie einen eindeutigen Eigentümer des Objekts, der die Lebensdauer verwaltet?
Bo Persson
2
Was ist mit explizit freigegebenen Daten? Es ist schwer, keine Zeiger zu verwenden. Auch std :: shared_pointer würde in diesem Fall die schmutzige "Lebensdauer verwalten"
Kamil Klimek
6
Haben Sie darüber nachgedacht, weniger den vorgelegten Ratschlägen zuzuhören, als vielmehr dem Argument, das hinter diesen Ratschlägen steht? Er erklärt ziemlich gut, in welchem ​​System solche Ratschläge funktionieren würden.
Nicol Bolas
@NicolBolas: Ich habe auf den Rat und das Argument gehört, aber offensichtlich habe ich nicht das Gefühl, dass ich es gut genug verstanden habe.
Ronag
Wann sagt er "letzter Ausweg"? Nach 36 Minuten in ( channel9.msdn.com/Events/GoingNative/GoingNative-2012/… ) sagt er, dass er die Verwendung von Zeigern misstraut , aber er meint Zeiger im Allgemeinen, nicht nur shared_ptr und unique_ptr, sondern sogar normaler Zeiger. Er impliziert, dass Objekte selbst (und keine Zeiger auf Objekte, die neu zugewiesen wurden) bevorzugt werden sollten. War das Stück, an das Sie später in der Präsentation gedacht haben?
Pharap

Antworten:

55

Wenn Sie das gemeinsame Eigentum 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 dazu, dass es schwierig ist, Kopplungen verschiedener Teile der Anwendung durch einen gemeinsamen Status zu verfolgen, der möglicherweise nicht einfach zu verfolgen ist.

In Anbetracht dessen ist es vorzuziehen, Objekte mit automatischer Speicherdauer zu verwenden und "Wert" -Unterobjekte zu haben. Gelingt dies nicht, ist dies unique_ptrmöglicherweise eine gute Alternative, da shared_ptres - wenn nicht das letzte Mittel - auf der Liste der erstrebenswerten Tools auftaucht.

CB Bailey
quelle
5
+1 dafür, dass es nicht um den Techno selbst (Shared Ownership) geht, sondern um die Schwierigkeiten, die er uns Menschen bereitet, die dann entschlüsseln müssen, was los ist.
Matthieu M.
Ein solcher Ansatz schränkt jedoch die Fähigkeit eines Programmierers zum Anwenden von parallelen Programmiermustern auf die meisten nicht trivialen OOP-Klassen (aufgrund der Nichtkopierbarkeit) erheblich ein. Dieses Problem wird in "Going Native 2013" angesprochen.
Rwong
48

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 über sehr bewusste relationale Hierarchien verfügen, sodass Eigentumsverhältnisse starr und unnachgiebig sind, fließt der Code in eine Richtung (von der oberen zur unteren Ebene), und Objekte kommunizieren nur mit denjenigen, die sich in der unteren Ebene befinden die Hierarchie, dann werden Sie nicht viel brauchen shared_ptr. Es ist etwas, das Sie in den seltenen Fällen verwenden, in denen jemand gegen die Regeln verstoßen muss. Ansonsten können Sie einfach alles in vectors oder andere Datenstrukturen einfügen, die Wertesemantik verwenden, und unique_ptrs für Dinge, die Sie einzeln zuweisen müssen.

Es ist zwar eine großartige Welt, in der man leben kann, aber nicht das, was man die ganze Zeit tun muss. Wenn Sie nicht können Ihren Code auf diese Weise organisieren, weil das Design des Systems Sie versuchen zu machen , bedeutet , dass es unmöglich ist (oder einfach nur tief unangenehm), dann wirst du finden sich gemeinsame Verantwortung für Objekte benötigen mehr und mehr .

In einem solchen System ist das Halten nackter Zeiger ... nicht gerade gefährlich, wirft jedoch Fragen auf. Das Tolle daran shared_ptrist, dass es angemessene syntaktische Garantien für die Lebensdauer des Objekts bietet . Kann es kaputt sein? Na sicher. Menschen können aber auch const_castDinge; Grundversorgung und Fütterung shared_ptrsollten eine angemessene Lebensqualität für zugewiesene Objekte gewährleisten, deren Eigentum geteilt werden muss.

Dann gibt es weak_ptrs, die ohne a nicht verwendet werden können shared_ptr. Wenn Ihr System starr strukturiert ist, können Sie einen nackten Zeiger auf ein Objekt speichern, in dem Wissen, 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 die Lebensdauer auf angemessene Weise sicherstellen . Manchmal brauchen Sie kein vollständiges Eigentum. Manchmal muss man nur wissen können, wann der Zeiger schlecht oder gut ist. Das ist , wo weak_ptrkommt in . Es gibt Fälle, wo ich könnte ein verwendet haben , unique_ptroder boost::scoped_ptr, aber ich hatte eine verwenden , shared_ptrweil ich speziell einen „flüchtigen“ Zeiger zu geben , jemand benötigen. Ein Zeiger, dessen Lebensdauer unbestimmt war, konnte abfragen, wann dieser Zeiger zerstört wurde.

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

Könnte das durch einen Funktionsaufruf gemacht worden sein, um den Zeiger anstatt über zu erhalten 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ücksendung von a shared_ptrmacht es auch viel zu einfach für jemanden, es einfach zu speichern und möglicherweise die Lebensdauer eines Objekts zu verlängern. Die Rückgabe eines weak_ptrjedoch deutet stark darauf hin, dass das Speichern der shared_ptrDaten, von denen Sie erhalten, lockeine ... zweifelhafte Idee ist. Es wird Sie nicht davon abhalten, es zu tun, aber nichts in C ++ hält Sie davon ab, Code zu brechen. weak_ptrbietet einen minimalen Widerstand gegen das Natürliche.

Das soll nicht heißen, dass shared_ptrman es nicht überbeanspruchen kann . es kann sicherlich. Vor allem vorhin unique_ptrgab es viele Fälle, in denen ich einfach a verwendet habe, boost::shared_ptrweil ich einen RAII-Zeiger herumreichen oder in eine Liste aufnehmen musste. Ohne Bewegungssemantik und unique_ptr, boost::shared_ptrwar die einzig wahre Lösung.

Und Sie können es an Stellen verwenden, an denen es nicht erforderlich ist. Wie oben angegeben, kann eine ordnungsgemäße Codestruktur die Notwendigkeit einiger Verwendungen von beseitigen shared_ptr. Aber wenn Ihr System nicht als solches strukturiert werden kann und dennoch das tut, was es benötigt, shared_ptrist dies von erheblichem Nutzen.

Nicol Bolas
quelle
4
+1: Schau dir zB boost :: asio an. Ich denke , die Idee auf viele Bereiche erstreckt, Sie nicht bei der Kompilierung , die UI - Widget oder asynchrone Aufruf ist einfach kann das letzte , ein Objekt zu verzichten, und mit shared_ptr Sie nicht brauchen zu wissen. Dies gilt natürlich nicht für jede Situation, sondern nur für ein weiteres (sehr nützliches) Tool in der Tool-Box.
Guy Sirton
3
Ein bisschen später Kommentar; shared_ptreignet sich hervorragend für Systeme, in denen C ++ in Skriptsprachen wie Python integriert ist. Die boost::pythonReferenzzählung auf der C ++ - und Python-Seite funktioniert bei der Verwendung sehr gut. Jedes Objekt aus C ++ kann weiterhin in Python gespeichert werden und stirbt nicht.
Eudoxos
1
Mein Verständnis bezieht sich lediglich auf die Verwendung von WebKit und Chromium shared_ptr. Beide verwenden ihre eigenen Implementierungen von intrusive_ptr. Ich
erwähne das
1
@gman: Ich finde Ihren Kommentar sehr irreführend, da Stroustrups Einwand shared_ptrgleichermaßen gilt für intrusive_ptr: Er lehnt das gesamte Konzept der gemeinsamen Eigentümerschaft ab, nicht eine bestimmte Schreibweise des Konzepts. Für die Zwecke dieser Frage sind dies zwei reale Beispiele für große Anwendungen, die verwendet werden shared_ptr. (Und außerdem zeigen sie, dass dies shared_ptrnützlich ist, auch wenn es nicht möglich ist weak_ptr.)
Ruakh
1
FWIW, um der Behauptung entgegenzuwirken, dass Bjarne in der akademischen Welt lebt: In all meiner rein industriellen Karriere (die das Co-Architecting einer G20-Börse und das reine Architecting eines MOG mit 500.000 Spielern beinhaltete) habe ich nur drei Fälle gesehen, in denen wir es wirklich brauchten geteilter Besitz. Ich bin zu 200% bei Bjarne.
No-Bugs Hare
37

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

Meistens ist ein Objekt mit einer Sammlung verbunden, zu der es während seiner gesamten Lebensdauer gehört. In diesem Fall können Sie einfach whatever_collection<o_type>oder verwenden whatever_collection<std::unique_ptr<o_type>>, wobei diese Auflistung 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 Auflistung noch eine andere Operation für das Objekt erfordert eine Hilfsfunktion, um den Besitz gemeinsam zu nutzen. Sie verwendet das Objekt und kehrt dann zurück. Der Aufrufer garantiert, dass das Objekt für den gesamten Aufruf am Leben bleibt . Dies ist mit Abstand der am häufigsten genutzte Vertrag zwischen Anrufer und Angerufenen.


Nicol Bolas kommentierte: "Wenn sich ein Objekt an einem nackten Zeiger festhält und dieser Gegenstand stirbt ... hoppla." und "Objekte müssen sicherstellen, dass das Objekt das Leben des Objekts durchlebt. Nur das shared_ptrkann es."

Ich kaufe dieses Argument nicht. Zumindest nicht das shared_ptrlöst dieses Problem. Wie wäre es mit:

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

Wie bei der Garbage Collection wird shared_ptrder Programmierer durch die Standardverwendung von dazu angeregt, nicht über den Vertrag zwischen Objekten oder zwischen Funktion und Aufrufer nachzudenken. Es ist notwendig, über die richtigen Vor- und Nachbedingungen nachzudenken, und die Objektlebensdauer ist nur ein winziges Stück dieser größeren Torte.

Objekte "sterben" nicht, ein Teil des Codes zerstört sie. Und shared_ptrdas Problem anzugehen, anstatt den Anrufvertrag herauszufinden, ist eine falsche Sicherheit.

Ben Voigt
quelle
17
@ronag: Ich vermute, dass Sie damit begonnen haben, wo ein roher Zeiger besser gewesen wäre, weil "rohe Zeiger schlecht sind". Aber rohe Zeiger sind nicht schlecht . Es ist schlecht, nur den ersten Zeiger , dessen Eigentümer ein Objekt ist, zu einem Rohzeiger zu machen, da Sie dann den Speicher manuell verwalten müssen, was bei Ausnahmen nicht trivial ist. Die Verwendung von rohen Zeigern als Ziehpunkte oder Iteratoren ist jedoch in Ordnung.
Ben Voigt
4
@ BenVoigt: Natürlich besteht die Schwierigkeit beim Weitergeben von nackten Zeigern darin, dass Sie die Lebensdauer von Objekten nicht kennen. Wenn sich ein Gegenstand an einem nackten Zeiger festhält und dieser Gegenstand stirbt ... hoppla. Das ist genau die Art der Sache shared_ptrund weak_ptrwurde entwickelt, um zu vermeiden. Bjarne versucht, in einer Welt zu leben, in der alles ein schönes, explizites Leben hat, und alles ist darauf aufgebaut. Und wenn du diese Welt bauen kannst, großartig. Aber so ist es nicht in der realen Welt. Objekte müssen sicherstellen, dass das Objekt das Leben des Objekts durchlebt. Nur shared_ptrkann das tun.
Nicol Bolas
5
@NicolBolas: Das ist falsche Sicherheit. Wenn der Aufrufer einer Funktion nicht die übliche Garantie bietet: "Dieses Objekt wird während des Funktionsaufrufs von keiner externen Partei berührt", müssen sich beide darauf einigen, welche Art von externen Änderungen zulässig sind. shared_ptrmildert nur eine bestimmte äußere Modifikation und nicht einmal die häufigste. Und es liegt nicht in der Verantwortung des Objekts, sicherzustellen, dass seine Lebensdauer korrekt ist, wenn der Funktionsaufrufvertrag etwas anderes bestimmt.
Ben Voigt
6
@NicolBolas: Wenn eine Funktion ein Objekt erstellt und es als Zeiger zurückgibt, sollte es ein sein unique_ptr, der ausdrückt, dass nur ein Zeiger auf das Objekt vorhanden ist und Eigentümer ist.
Ben Voigt
6
@Nicol: Wenn ein Zeiger in einer Sammlung nachgeschlagen wird, sollte er wahrscheinlich den Zeigertyp in dieser Sammlung oder einen Rohzeiger verwenden, wenn die Sammlung Werte enthält. Wenn es ein Objekt erstellt und der Aufrufer ein möchte shared_ptr, sollte es trotzdem ein zurückgeben unique_ptr. Die Konvertierung von unique_ptrnach shared_ptrist einfach, aber das Gegenteil ist logisch unmöglich.
Ben Voigt
16

Ich ziehe es vor, nicht absolut zu denken (wie "letzter Ausweg"), sondern relativ zur Problemdomäne.

C ++ bietet eine Reihe von Möglichkeiten zur Verwaltung der Lebensdauer. Einige von ihnen versuchen, die Objekte auf stapelgesteuerte Weise wiederzuerlangen. Einige andere versuchen, sich dieser Einschränkung zu entziehen. Einige von ihnen sind "wörtlich", andere sind Annäherungen.

Eigentlich können Sie:

  1. Verwenden Sie reine Wertsemantik . Arbeitet für relativ kleine Objekte in dem , was wichtig ist „Wert“ und nicht „Identitäten“ sind, wo man davon ausgehen kann , dass zwei Personeine gleiche mit nameder gleichen Person (besser: zwei Darstellung einer gleichen Person ). Lebensdauer wird vom Maschinenstapel, Ende -essentially- deosn't Angelegenheit des Programm gewährt (weil eine Person ist es s Name , egal , was Persones trägt)
  2. Verwenden Sie stapelzugeordnete Objekte und verwandte Verweise oder Zeiger: Ermöglicht Polymorphismus und gewährt die Objektlebensdauer. Sie benötigen keine "intelligenten 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 (erst das Objekt erstellen, dann die Strukturen, die darauf verweisen).
  3. Verwenden Sie stapelverwaltete Heap-zugewiesene Objekte : Dies ist das, was std :: vector und alle Container tun, und wat std::unique_ptr(Sie können sich das als einen Vektor mit Größe 1 vorstellen). Wieder geben Sie zu, dass das Objekt vor (nach) der Datenstruktur, auf die es verweist, zu existieren beginnt (und ihre Existenz beendet).

Die Schwäche dieser Methoden besteht darin, dass sich Objekttypen und -mengen während der Ausführung von Aufrufen auf tieferer Stapelebene in Bezug auf den Ort, an dem sie erstellt wurden, nicht unterscheiden können. Alle diese Techniken "scheitern" an ihrer Stärke in allen Situationen, in denen das Erstellen und Löschen von Objekten auf Benutzeraktivitäten zurückzuführen ist, so dass der Laufzeit-Typ des Objekts zur Kompilierungszeit nicht bekannt ist und es zu Überstrukturen kommen kann, die auf Objekte verweisen Benutzer bittet, aus einem tieferen Funktionsaufruf auf Stapelebene zu entfernen. In diesen Fällen müssen Sie entweder:

  • Etwas Disziplin über das Verwalten von Objekten und verwandten verweisenden Strukturen einführen oder ...
  • Gehen Sie irgendwie auf die dunkle Seite von "Entfliehen Sie dem reinen stapelbasierten Leben": Das Objekt muss unabhängig von den Funktionen, die es erstellt haben, verlassen werden. Und muss gehen ... bis sie gebraucht werden .

C ++ isteslf hat keinen systemeigenen Mechanismus zum Überwachen dieses Ereignisses ( while(are_they_needed)), daher müssen Sie ungefähr mit:

  1. Gemeinsames Eigentum verwenden : Das Leben von Objekten ist an einen "Referenzzähler" gebunden: Funktioniert, wenn das "Eigentum" hierarchisch organisiert werden kann, und schlägt fehl, wenn Besitzschleifen vorhanden sein können. Das macht std :: shared_ptr. Und weak_ptr kann verwendet werden, um die Schleife zu unterbrechen. Dies funktioniert die meiste Zeit, scheitert jedoch bei großem Design, bei dem viele Designer in verschiedenen Teams arbeiten und es keinen eindeutigen Grund (etwas, das von einer gewissen Anforderung herrührt) dafür gibt, wer muffelig ist, was zu besitzen (das typische Beispiel sind Ketten mit doppelter Sympathie: ist das vorherige aufgrund der nächsten Bezugnahme die vorherige oder die nächste Eigentümer der vorherigen Bezugnahme die nächste? In Abwesenheit einer Anforderung sind die tho-Lösungen gleichwertig, und bei großen Projekten besteht die Gefahr, dass Sie sie verwechseln.)
  2. Verwenden Sie einen Müllhaufen : Sie kümmern sich einfach nicht um die Lebensdauer. Sie lassen den Sammler von Zeit zu Zeit laufen und was unerreichbar 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 keinen, der wirklich C ++ kennt. Die meisten von ihnen haben freien Speicherplatz und kümmern sich nicht um die Zerstörung von Objekten.
  3. Verwenden Sie einen C ++ - fähigen Garbage Collector mit einer geeigneten Standardmethodenschnittstelle. Viel Glück, es zu finden.

Bei der allerersten bis zur letzten Lösung nimmt die Menge der für die Verwaltung der Objektlebensdauer erforderlichen Hilfsdatenstruktur mit dem Zeitaufwand für deren Organisation und Wartung zu.

Der Garbage Collector hat Kosten, shared_ptr hat weniger, unique_ptr noch weniger und Stack-verwaltete Objekte haben nur sehr wenige.

Ist shared_ptrder "letzte Ausweg" ?. Nein, das ist es nicht: Die letzten Mittel sind Müllsammler. shared_ptrist eigentlich der std::letzte Ausweg vorgeschlagen. Aber kann die richtige Lösung sein, wenn Sie in der Situation sind, die ich erklärte.

Emilio Garavaglia
quelle
9

Die eine Sache, die Herb Sutter in einer späteren Sitzung erwähnte, ist, dass jedes Mal, wenn Sie eine kopieren shared_ptr<>, ein ineinandergreifendes Inkrement / Dekrement auftreten muss. Bei Multithread-Code auf einem Multi-Core-System ist die Speichersynchronisation nicht unerheblich. Wenn Sie die Wahl haben, ist es besser, entweder einen Stapelwert oder einen zu verwenden unique_ptr<>und Referenzen oder rohe Zeiger zu übergeben.

Finsternis
quelle
1
Oder shared_ptrlvalue oder rvalue Referenz übergeben ...
Ronag
8
Der Punkt ist, nicht nur zu verwenden, shared_ptrals wäre es die Silberkugel, die all Ihre Speicherleckprobleme löst, nur weil es im Standard enthalten ist. Es ist eine verlockende Falle, aber es ist immer noch wichtig, sich des Eigentums an Ressourcen bewusst zu sein. Wenn dieses Eigentum nicht geteilt wird, shared_ptr<>ist a nicht die beste Option.
Eclipse -
Für mich ist dies das unwichtigste Detail. Siehe vorzeitige Optimierung. In den meisten Fällen sollte dies nicht ausschlaggebend sein.
Guy Sirton
1
@gbjbaanb: Ja, sie sind auf der CPU-Ebene, aber auf einem Multi-Core-System machen Sie Caches ungültig und erzwingen Speicherbarrieren.
Eclipse
4
In einem Spielprojekt, an dem ich gearbeitet habe, stellten wir fest, dass der Leistungsunterschied sehr bedeutend war, bis zu dem Punkt, an dem wir zwei verschiedene Arten von ref-counted-Zeigern benötigten, einer, der threadsicher war, einer, der es nicht war.
Kylotan
7

Ich erinnere mich nicht, ob der letzte "Ausweg" das genaue Wort war, das er benutzte, aber ich glaube, dass die tatsächliche Bedeutung dessen, was er sagte, die letzte "Wahl" war: Angesichts klarer Besitzverhältnisse; unique_ptr, weak_ptr, shared_ptr und sogar nackte Zeiger haben ihren Platz.

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

Als Beispiel, erklärte Herb, sollten wir neue Ausgaben einiger der wichtigsten C ++ - Bücher erwarten, wie z. B. Effective C ++ (Meyers) und C ++ - Coding-Standards (Sutter & Alexandrescu), die einige Jahre nach der Erfahrung und den Best Practices der Branche mit C liegen ++ 11 schwenkt aus.

Eddie Velasquez
quelle
5

Ich denke, was er damit meint, ist, dass es üblich wird, shared_ptr immer dann zu schreiben, wenn sie einen Standardzeiger geschrieben haben (wie eine Art globaler Ersatz), und dass es als Copout verwendet wird, anstatt tatsächlich zu entwerfen oder zumindest Planung für das Anlegen und Löschen von Objekten.

Das andere, was die Leute vergessen (abgesehen von dem oben erwähnten Engpass beim Sperren / Aktualisieren / Entsperren), ist, dass shared_ptr alleine keine Zyklusprobleme löst. Sie können mit shared_ptr weiterhin Ressourcen verlieren:

Objekt A enthält einen gemeinsamen Zeiger auf ein anderes Objekt. Objekt B erstellt A a1 und A a2 und weist die Werte a1.otherA = a2 zu. und a2.otherA = a1; Die gemeinsamen Zeiger von Objekt B, die zum Erstellen von a1, a2 verwendet wurden, haben den Gültigkeitsbereich verlassen (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 ein Leck haben.

Das ist das einfache Beispiel, wenn dies in echtem Code auftritt, geschieht dies normalerweise auf komplizierte Weise. Es gibt eine Lösung für weak_ptr, aber so viele Leute machen jetzt einfach shared_ptr überall und kennen nicht einmal das Leckproblem oder auch nur weak_ptr.

Um es zusammenzufassen: Ich denke, die Kommentare, auf die sich das OP bezieht, beschränken sich auf Folgendes:

Unabhä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 "unbekannt" bedeutet, muss ich einen shared_ptr verwenden, du hast immer noch darüber nachgedacht und tust es absichtlich.

anon
quelle
3

Ich werde aus meiner Erfahrung mit Objective-C antworten, einer Sprache, in der alle Objekte auf dem Heap referenziert und zugeordnet werden. Für den Programmierer ist es viel einfacher, Objekte auf eine Weise zu behandeln. Dadurch konnten Standardregeln definiert werden, die bei Einhaltung der Regeln Code-Robustheit und keine Speicherverluste gewährleisten. Es war auch möglich, dass clevere Compiler-Optimierungen wie die aktuelle ARC (Automatic Reference Counting) auftauchten.

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

Dimitris
quelle
1

Ich werde versuchen, die Frage zu beantworten:

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

In C ++ gibt es eine große Anzahl von verschiedenen Möglichkeiten, um Speicher zu erstellen, zum Beispiel:

  1. Verwenden Sie struct A { MyStruct s1,s2; };anstelle von shared_ptr im Klassenbereich. Dies gilt nur für fortgeschrittene Programmierer, da Sie wissen müssen, wie Abhängigkeiten funktionieren, und die Fähigkeit benötigen, Abhängigkeiten so weit zu steuern, dass sie auf einen Baum beschränkt sind. Die Reihenfolge der Klassen in der Headerdatei ist dabei ein wichtiger Aspekt. Es scheint, dass diese Verwendung bereits bei systemeigenen C ++ - Typen üblich ist, aber die Verwendung mit vom Programmierer definierten Klassen scheint aufgrund dieser Abhängigkeit und der Reihenfolge der Klassenprobleme weniger häufig zu sein. Diese Lösung hat auch Probleme mit sizeof. Programmierer sehen Probleme darin als eine Anforderung, Forward-Deklarationen oder unnötige # include-Anweisungen zu verwenden, und daher greifen viele Programmierer auf eine minderwertige Lösung von Zeigern und später auf shared_ptr zurück.
  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 Nutzung geeignet. Das Problem ist, dass es bereits eine komplexe Speicherverwaltungslösung mit Heap / Free Store Allocation anstelle einer einfacheren stapel- oder objektbasierten Lösung annimmt. 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 Weitergabe des Eigentums von der Fabrik an den Benutzercode sollte vermieden werden. Das Kopieren des Objekts nach der Verwendung von find_obj () ist eine gute Möglichkeit, damit umzugehen - normale Kopierkonstruktoren und normale Konstruktoren (verschiedener Klassen) mit dem Referenzparameter 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 anhand der Entscheidung die Struktur- oder Referenzlösung auswählen. Probleme mit dieser Lösung hängen normalerweise mit dem Vermeiden von Konstruktorparametern zusammen, was üblich, aber problematisch ist und mit dem Missverständnis, wie Schnittstellen für Klassen entworfen werden sollten.
tp1
quelle
"Die Weitergabe des Eigentums von der Fabrik an den Benutzercode sollte vermieden werden." Und was passiert, wenn das nicht möglich ist? "Verwendung von Referenzen anstelle von Zeigern oder shared_ptrs." Ähm, nein. Zeiger können neu platziert werden. Referenzen können nicht. Dies erzwingt eine Einschränkung der Bauzeit für das, was in einer Klasse gespeichert ist. Das ist für viele Dinge nicht praktikabel. Ihre Lösung scheint sehr starr und unflexibel für die Anforderungen einer flüssigeren Schnittstelle und eines flüssigeren Verwendungsmusters zu sein.
Nicol Bolas
@Nicol Bolas: Sobald Sie die obigen Regeln befolgen, werden die Referenzen für Abhängigkeiten zwischen Objekten verwendet und nicht für die Datenspeicherung, wie Sie vorgeschlagen haben. Die Abhängigkeiten sind stabiler als die Daten, sodass wir nie auf das Problem eingehen, das Sie in Betracht gezogen haben.
tp1
Hier ist ein sehr einfaches Beispiel. Sie haben eine Spieleinheit, die ein Objekt ist. Es muss auf ein anderes Objekt verweisen, bei dem es sich um eine Zieleinheit handelt, mit der es kommunizieren muss. Ziele können sich jedoch ändern. Ziele können an verschiedenen Punkten sterben . Und das Unternehmen muss in der Lage sein, mit diesen Umständen umzugehen. Ihr starrer No-Pointers-Ansatz kann nicht einmal etwas so Einfaches handhaben wie das Ändern von Zielen, geschweige denn das Sterben des Ziels.
Nicol Bolas
@nicol bolas: oh, das wird anders gehandhabt; Die Schnittstelle der Klasse unterstützt mehr als eine "Entität". Anstelle einer 1: 1-Zuordnung zwischen Objekten und Entitäten verwenden Sie das Entityarray. Dann sterben Entitäten sehr leicht, indem sie einfach aus dem Array entfernt werden. Es gibt nur eine kleine Anzahl von Entityarrays im gesamten Spiel und die Abhängigkeiten zwischen Arrays ändern sich nicht sehr oft :)
tp1
2
Nein, unique_ptrist am besten für Fabriken geeignet. Sie können eine unique_ptrin eine verwandeln shared_ptr, aber es ist logischerweise unmöglich, in die andere Richtung zu gehen.
Ben Voigt