Welche Art von Zeiger verwende ich wann?

227

Ok, das letzte Mal, als ich C ++ für std::auto_ptrmeinen Lebensunterhalt schrieb, war alles, was die Standardbibliothek zur Verfügung hatte, und boost::shared_ptrwar der letzte Schrei. Ich habe mich nie wirklich mit den anderen Boosts für intelligente Zeigertypen befasst. Ich verstehe, dass C ++ 11 jetzt einige der Typen bietet, die Boost entwickelt hat, aber nicht alle.

Hat jemand einen einfachen Algorithmus, um zu bestimmen, wann welcher intelligente Zeiger verwendet werden soll? Vorzugsweise mit Hinweisen zu dummen Zeigern (wie rohen Zeigern T*) und dem Rest der Boost-Smart-Zeiger. (So etwas wie das wäre toll).

sbi
quelle
Siehe auch std :: auto_ptr bis std :: unique_ptr
Martin York
1
Ich hoffe wirklich, dass jemand ein schönes handliches Flussdiagramm wie dieses STL-Auswahlflussdiagramm erstellt .
Alok Save
1
@Als: Oh, das ist in der Tat eine schöne! Ich habe es FAQisiert.
sbi
6
@Deduplicator Das ist nicht einmal annähernd ein Duplikat. Die verknüpfte Frage lautet "Wann sollte ich einen intelligenten Zeiger verwenden ?" Und diese Frage lautet "Wann verwende ich diese intelligenten Zeiger?". dh dieser kategorisiert die verschiedenen Verwendungen der Standard-Smart-Zeiger. Die verknüpfte Frage tut dies nicht. Der Unterschied scheint klein zu sein, aber er ist groß.
Rapptz

Antworten:

183

Shared Ownership:
Der shared_ptrund weak_ptrder angenommene Standard entsprechen weitgehend denen der Boost-Kollegen . Verwenden Sie sie, wenn Sie eine Ressource freigeben müssen und nicht wissen, welche als letzte am Leben sein wird. Verwenden Sie weak_ptrdiese Option , um die gemeinsam genutzte Ressource zu beobachten, ohne ihre Lebensdauer zu beeinflussen, und um Zyklen nicht zu unterbrechen. Zyklen mit shared_ptrsollten normalerweise nicht stattfinden - zwei Ressourcen können sich nicht besitzen.

Beachten Sie, dass Boost zusätzlich Angebote bietet shared_array, die eine geeignete Alternative zu sein könnten shared_ptr<std::vector<T> const>.

Als nächstes bietet Boost Angebote an intrusive_ptr, die eine einfache Lösung darstellen, wenn Ihre Ressource bereits ein Management mit Referenzzählung bietet und Sie es in das RAII-Prinzip übernehmen möchten. Dieser wurde vom Standard nicht übernommen.

Einzigartiges Eigentum:
Boost hat auch ein scoped_ptr, das nicht kopierbar ist und für das Sie keinen Deleter angeben können. std::unique_ptrist boost::scoped_ptrauf Steroiden und sollte Ihre Standardwahl sein, wenn Sie einen intelligenten Zeiger benötigen . Es ermöglicht Ihnen, einen Deleter in seinen Vorlagenargumenten anzugeben und ist im Gegensatz zu beweglichboost::scoped_ptr . Es kann auch vollständig in STL-Containern verwendet werden, solange Sie keine Vorgänge verwenden, für die (offensichtlich) kopierbare Typen erforderlich sind.

Beachten Sie erneut, dass Boost eine Array-Version hat : scoped_array, die der Standard vereinheitlicht, indem eine std::unique_ptr<T[]>teilweise Spezialisierung erforderlich ist, die delete[]den Zeiger anstelle des delete(mit dem default_deleter) verwendet. std::unique_ptr<T[]>bietet auch operator[]anstelle von operator*und operator->.

Beachten Sie, dass dies std::auto_ptrnoch im Standard enthalten ist, jedoch veraltet ist . §D.10 [depr.auto.ptr]

Die Klassenvorlage auto_ptrist veraltet. [ Hinweis: Die Klassenvorlage unique_ptr(20.7.1) bietet eine bessere Lösung. - Endnote ]

Kein Besitz:
Verwenden Sie dumme Zeiger (Rohzeiger) oder Referenzen für nicht besitzende Verweise auf Ressourcen und wenn Sie wissen, dass die Ressource das referenzierende Objekt / den überlebenden Bereich überlebt . Bevorzugen Sie Referenzen und verwenden Sie Rohzeiger, wenn Sie entweder Nullbarkeit oder Rücksetzbarkeit benötigen.

Wenn Sie eine nicht-besitzende Referenz auf eine Ressource, aber Sie wissen nicht , ob die Ressource das Objekt überleben wird , dass Verweise darauf in einer die Ressource packen shared_ptrund verwenden weak_ptr- Sie können testen , ob die Eltern shared_ptrleben mit lock, die Geben Sie a zurück shared_ptr, das nicht null ist, wenn die Ressource noch vorhanden ist. Wenn Sie testen möchten, ob die Ressource tot ist, verwenden Sie expired. Die beiden mögen ähnlich klingen, unterscheiden sich jedoch bei gleichzeitiger Ausführung erheblich, da expirednur der Rückgabewert für diese einzelne Anweisung garantiert wird. Ein scheinbar unschuldiger Test wie

if(!wptr.expired())
  something_assuming_the_resource_is_still_alive();

ist eine mögliche Rennbedingung.

Xeo
quelle
1
Wenn Sie kein Eigentum haben, sollten Sie wahrscheinlich Verweise auf Zeiger bevorzugen, es sei denn, Sie benötigen kein Eigentum und keine Rücksetzbarkeit, wenn Referenzen es nicht schneiden. Selbst dann sollten Sie in Betracht ziehen, das ursprüngliche Objekt als a shared_ptrund den nicht besitzenden Zeiger neu zu schreiben a weak_ptr...
David Rodríguez - Dribeas
2
Ich meinte nicht Verweis auf Zeiger , sondern Verweis statt Zeiger. Wenn es keine Eigentumsverhältnisse gibt, können Sie zunächst eine einfache Referenz anstelle eines Zeigers verwenden, es sei denn, Sie benötigen eine Rücksetzbarkeit (oder eine Nullfähigkeit, aber die Nullfähigkeit ohne Zurücksetzen wäre ziemlich eingeschränkt).
David Rodríguez - Dribeas
1
@ David: Ah, ich verstehe. :) Ja, Referenzen sind dafür nicht schlecht, ich persönlich bevorzuge sie auch in solchen Fällen. Ich werde sie hinzufügen.
Xeo
1
@Xeo: shared_array<T>ist eine Alternative zu shared_ptr<T[]>nicht zu shared_ptr<vector<T>>: es kann nicht wachsen.
R. Martinho Fernandes
1
@ GregroyCurrie: Genau das habe ich geschrieben? Ich sagte, es ist ein Beispiel für eine mögliche Rennbedingung.
Xeo
127

Die Entscheidung, welcher intelligente Zeiger verwendet werden soll, ist eine Frage des Eigentums . Wenn es um die Ressourcenverwaltung geht, besitzt Objekt A Objekt B, wenn es die Lebensdauer von Objekt B kontrolliert. Beispielsweise gehören Mitgliedsvariablen ihren jeweiligen Objekten, da die Lebensdauer von Mitgliedsvariablen an die Lebensdauer des Objekts gebunden ist. Sie wählen intelligente Zeiger basierend darauf, wie das Objekt gehört.

Beachten Sie, dass das Eigentum an einem Softwaresystem vom Eigentum getrennt ist, da wir es außerhalb von Software betrachten würden. Zum Beispiel könnte eine Person ihr Haus "besitzen", aber das bedeutet nicht unbedingt, dass ein PersonObjekt die Kontrolle über die Lebensdauer eines HouseObjekts hat. Das Zusammenführen dieser realen Konzepte mit Softwarekonzepten ist ein sicherer Weg, sich selbst in ein Loch zu programmieren.


Wenn Sie das alleinige Eigentum an dem Objekt haben, verwenden Sie std::unique_ptr<T>.

Wenn Sie das Eigentum gemeinsam genutzt haben ...
- Wenn keine Eigentumszyklen vorhanden sind, verwenden Sie std::shared_ptr<T>.
- Wenn es Zyklen gibt, definieren Sie eine "Richtung" und verwenden Sie std::shared_ptr<T>in die eine und std::weak_ptr<T>in die andere Richtung.

Wenn Ihnen das Objekt gehört, Sie jedoch möglicherweise keinen Eigentümer haben, verwenden Sie normale Zeiger T*(z. B. übergeordnete Zeiger).

Wenn das Objekt Ihnen gehört (oder anderweitig die Existenz garantiert hat), verwenden Sie Referenzen T&.


Vorsichtsmaßnahme: Beachten Sie die Kosten für intelligente Zeiger. In speicher- oder leistungsbeschränkten Umgebungen kann es vorteilhaft sein, nur normale Zeiger mit einem manuelleren Schema für die Speicherverwaltung zu verwenden.

Die Kosten:

  • Wenn Sie einen benutzerdefinierten Löscher haben (z. B. Zuordnungspools verwenden), entsteht ein Overhead pro Zeiger, der durch manuelles Löschen leicht vermieden werden kann.
  • std::shared_ptrhat den Overhead eines Referenzzählungsinkrements beim Kopieren plus eines Dekrements bei der Zerstörung, gefolgt von einer 0-Zählprüfung mit Löschen des gehaltenen Objekts. Abhängig von der Implementierung kann dies Ihren Code aufblähen und Leistungsprobleme verursachen.
  • Kompilierungszeit. Wie bei allen Vorlagen tragen intelligente Zeiger negativ zur Kompilierungszeit bei.

Beispiele:

struct BinaryTree
{
    Tree* m_parent;
    std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};

Ein binärer Baum besitzt nicht seinen übergeordneten Baum, aber die Existenz eines Baums impliziert die Existenz seines übergeordneten Baums (oder nullptrfür root), sodass ein normaler Zeiger verwendet wird. Ein binärer Baum (mit Wertesemantik) hat das alleinige Eigentum an seinen untergeordneten Elementen std::unique_ptr.

struct ListNode
{
    std::shared_ptr<ListNode> m_next;
    std::weak_ptr<ListNode> m_prev;
};

Hier besitzt der Listenknoten seine nächsten und vorherigen Listen, daher definieren wir eine Richtung und verwenden sie shared_ptrfür next und weak_ptrfür prev, um den Zyklus zu unterbrechen.

Peter Alexander
quelle
3
Für das Beispiel des Binärbaums würden einige Leute vorschlagen, shared_ptr<BinaryTree>für die Kinder und weak_ptr<BinaryTree>für die Elternbeziehung zu verwenden.
David Rodríguez - Dribeas
@ DavidRodríguez-dribeas: Es hängt davon ab, ob der Baum eine Wertsemantik hat oder nicht. Wenn Leute Ihren Baum auch dann extern referenzieren, wenn der Quellbaum zerstört ist, ist eine Kombination aus geteiltem und schwachem Zeiger am besten.
Peter Alexander
Wenn ein Objekt Sie besitzt und garantiert existiert, warum dann nicht eine Referenz?
Martin York
1
Wenn Sie eine Referenz verwenden, können Sie das übergeordnete Element niemals ändern, was das Design möglicherweise behindert oder nicht. Für das Balancieren von Bäumen würde das behindern.
Mooing Duck
3
+1, aber Sie sollten in der ersten Zeile eine Definition von "Eigentum" hinzufügen. Ich muss oft klar sagen, dass es um Leben und Tod des Objekts geht, nicht um Eigentum in einer domänenspezifischeren Bedeutung.
Klaim
19

Verwenden Sie die unique_ptr<T>ganze Zeit, außer wenn Sie eine Referenzzählung benötigen. In diesem Fall verwenden Sie shared_ptr<T>(und in sehr seltenen Fällen, weak_ptr<T>um Referenzzyklen zu vermeiden). In fast allen Fällen ist übertragbares einzigartiges Eigentum in Ordnung.

Rohe Zeiger: Nur dann gut, wenn Sie kovariante Renditen benötigen. Sie sind sonst nicht besonders nützlich.

Array-Zeiger: unique_ptrhat eine Spezialisierung, für T[]die delete[]das Ergebnis automatisch aufgerufen wird , sodass Sie dies unique_ptr<int[]> p(new int[42]);beispielsweise sicher tun können . shared_ptrSie benötigen weiterhin einen benutzerdefinierten Löscher, benötigen jedoch keinen speziellen gemeinsam genutzten oder eindeutigen Array-Zeiger. Natürlich werden solche Dinge normalerweise am besten durch std::vectorsowieso ersetzt. Bietet leider shared_ptrkeine Array-Zugriffsfunktion, sodass Sie immer noch manuell aufrufen müssen get(), unique_ptr<T[]>bietet jedoch operator[]anstelle von operator*und operator->. In jedem Fall müssen Sie sich selbst überprüfen. Dies macht shared_ptretwas weniger benutzerfreundlich, obwohl wohl der generische Vorteil und keine Boost-Abhängigkeit macht unique_ptrund shared_ptrdie Gewinner wieder.

Zeiger mit Gültigkeitsbereich: irrelevant gemacht durch unique_ptr, genau wie auto_ptr.

Da ist wirklich nichts mehr dran. In C ++ 03 ohne Verschiebungssemantik war diese Situation sehr kompliziert, aber in C ++ 11 ist der Rat sehr einfach.

Es gibt immer noch Verwendungen für andere intelligente Zeiger wie intrusive_ptroder interprocess_ptr. Sie sind jedoch sehr nisch und im allgemeinen Fall völlig unnötig.

Hündchen
quelle
Auch rohe Zeiger für die Iteration. Und für Ausgabeparameterpuffer, bei denen der Puffer dem Aufrufer gehört.
Ben Voigt
Hmm, so wie ich das gelesen habe, sind es Situationen, die sowohl kovariante Rückkehr als auch Nicht-Besitz sind. Ein Umschreiben könnte gut sein, wenn Sie eher die Vereinigung als die Kreuzung meinen. Ich würde auch sagen, dass die Iteration ebenfalls eine besondere Erwähnung wert ist.
Ben Voigt
2
std::unique_ptr<T[]>bietet operator[]anstelle von operator*und operator->. Es ist wahr, dass Sie immer noch gebundene Überprüfungen durchführen müssen.
Xeo
8

Fälle, wann zu verwenden unique_ptr:

  • Fabrikmethoden
  • Mitglieder, die Zeiger sind (Pickel enthalten)
  • Speichern von Zeigern in STL-Containern (um Bewegungen zu vermeiden)
  • Verwendung großer lokaler dynamischer Objekte

Fälle, wann zu verwenden shared_ptr:

  • Objekte über Threads hinweg teilen
  • Objekte im Allgemeinen teilen

Fälle, wann zu verwenden weak_ptr:

  • Große Karte, die als allgemeine Referenz dient (z. B. eine Karte aller offenen Steckdosen)

Fühlen Sie sich frei zu bearbeiten und weitere hinzuzufügen

La La Land
quelle
Ihre Antwort gefällt mir tatsächlich besser, wenn Sie Szenarien angeben.
Nicholas Humphrey