Wann wird shared_ptr verwendet und wann werden Rohzeiger verwendet?

78
class B;

class A
{
public:
    A ()
        : m_b(new B())
    {
    }

    shared_ptr<B> GimmeB ()
    {
        return m_b;
    }

private:
    shared_ptr<B> m_b;
};

Nehmen wir an, B ist eine Klasse, die semantisch nicht außerhalb der Lebensdauer von A existieren sollte, dh es macht absolut keinen Sinn, dass B für sich allein existiert. Sollte GimmeBein shared_ptr<B>oder ein zurückgeben B*?

Ist es im Allgemeinen empfehlenswert, die Verwendung von Rohzeigern in C ++ - Code anstelle von intelligenten Zeigern vollständig zu vermeiden?

Ich bin der Meinung, dass dies shared_ptrnur verwendet werden sollte, wenn eine explizite Übertragung oder Aufteilung des Eigentums erfolgt, was meiner Meinung nach außerhalb von Fällen, in denen eine Funktion Speicher reserviert, mit einigen Daten füllt und zurückgibt und Verständnis besteht, ziemlich selten ist zwischen dem Anrufer und dem Angerufenen, dass der erstere jetzt für diese Daten "verantwortlich" ist.

TripShock
quelle
Ich gehe davon aus, dass es nicht möglich ist, Referenzen anstelle von Zeigern zurückzugeben.
Ricibob
Das ist ein guter Punkt ... Nehmen wir zur Diskussion an, dass ein Zeiger zurückgegeben werden muss.
TripShock
Wenn ich eine verwandte / sehr ähnliche Frage stellen möchte, ist es akzeptabel, diese zu bearbeiten und hier hinzuzufügen, oder sollte ich sie als eine andere Frage stellen?
TripShock

Antworten:

75

Ihre Analyse ist ganz richtig, denke ich. In dieser Situation würde ich auch ein nacktes zurückgeben B*, oder sogar ein, [const] B&wenn das Objekt garantiert niemals null ist.

Nachdem ich einige Zeit hatte, um intelligente Zeiger zu lesen, kam ich zu einigen Richtlinien, die mir in vielen Fällen sagen, was ich tun soll:

  • Wenn Sie ein Objekt zurückgeben, dessen Lebensdauer vom Aufrufer verwaltet werden soll, geben Sie return zurück std::unique_ptr. Der Anrufer kann es einem zuweisen, std::shared_ptrwenn er möchte.
  • Die Rückgabe std::shared_ptrist eigentlich ziemlich selten, und wenn es sinnvoll ist, ist es im Allgemeinen offensichtlich: Sie geben dem Aufrufer an, dass dies die Lebensdauer des Objekts, auf das verwiesen wird, über die Lebensdauer des Objekts hinaus verlängert, das ursprünglich die Ressource verwaltet hat. Die Rückgabe gemeinsamer Zeiger aus Fabriken ist keine Ausnahme: Sie müssen dies tun, z. wenn Sie verwenden std::enable_shared_from_this.
  • Sie brauchen sehr selten std::weak_ptr, außer wenn Sie die lockMethode verstehen wollen . Dies hat einige Verwendungszwecke, aber sie sind selten. Wenn in Ihrem Beispiel die Lebensdauer des AObjekts aus Sicht des Aufrufers nicht deterministisch gewesen wäre, wäre dies zu berücksichtigen gewesen.
  • Wenn Sie eine Referenz auf ein vorhandenes Objekt zurückgeben, dessen Lebensdauer der Aufrufer nicht steuern kann, geben Sie einen bloßen Zeiger oder eine Referenz zurück. Auf diese Weise teilen Sie dem Anrufer mit, dass ein Objekt vorhanden ist und dass er sich nicht um seine Lebensdauer kümmern muss. Sie sollten eine Referenz zurückgeben, wenn Sie den nullptrWert nicht verwenden.
Alexandre C.
quelle
5
" verlängert die Lebensdauer " " braucht sehr seltenstd::weak_ptr " Hören! hören!
Neugieriger
In der letzten Kugel gehen Sie auch davon aus, dass der Anrufer die Lebensdauer des Angerufenen kennt oder steuern kann. Wenn der Anrufer dies nicht tut oder nicht kann, sollten Sie einen intelligenten Zeiger zurückgeben.
Shital Shah
@ShitalShah: Wenn der Anrufer die Lebensdauer kontrolliert, machen Sie dies explizit mit std::unique_ptr(oder vielleicht habe ich nicht ganz verstanden, was Sie meinten).
Alexandre C.
1
sollte ich shared_ptr vom Factory-Design-Muster zurückgeben?
Stav Alfi
29

Die Frage "Wann soll ich verwenden shared_ptrund wann soll ich Rohzeiger verwenden?" hat eine sehr einfache Antwort:

  • Verwenden Sie unformatierte Zeiger, wenn Sie keine Eigentumsrechte an den Zeiger haben möchten. Diese Arbeit kann oft auch mit Referenzen erledigt werden. Raw-Zeiger können auch in einem Code auf niedriger Ebene verwendet werden (z. B. zum Implementieren von intelligenten Zeigern oder zum Implementieren von Containern).
  • Verwenden Sie unique_ptroder, scope_ptrwenn Sie das Objekt eindeutig besitzen möchten. Dies ist die nützlichste Option und sollte in den meisten Fällen verwendet werden. Eindeutiges Eigentum kann auch ausgedrückt werden, indem einfach ein Objekt direkt erstellt wird, anstatt einen Zeiger zu verwenden (dies ist sogar besser als die Verwendung von a unique_ptr, wenn dies möglich ist).
  • Verwenden Sie shared_ptroder, intrusive_ptrwenn Sie den Zeiger gemeinsam besitzen möchten. Dies kann verwirrend und ineffizient sein und ist oft keine gute Option. Shared Ownership kann in einigen komplexen Designs nützlich sein, sollte jedoch generell vermieden werden, da dies zu schwer verständlichem Code führt.

shared_ptrs führen eine völlig andere Aufgabe aus als Rohzeiger, und weder shared_ptrs noch Rohzeiger sind die beste Option für den Großteil des Codes.

Mankarse
quelle
11

Folgendes ist eine gute Faustregel:

  • Wenn keine Übertragung erfolgt oder gemeinsame Eigentumsreferenzen oder einfache Zeiger gut genug sind. (Einfache Zeiger sind flexibler als Referenzen.)
  • Wenn ein Eigentumsübergang stattfindet, aber kein gemeinsames Eigentum, std::unique_ptr<>ist dies eine gute Wahl. Oft der Fall bei Werksfunktionen.
  • Wenn es gemeinsames Eigentum gibt, ist dies ein guter Anwendungsfall für std::shared_ptr<>oder boost::intrusive_ptr<>.

Es ist am besten, gemeinsames Eigentum zu vermeiden, zum Teil, weil sie im Hinblick auf das Kopieren am teuersten sind und std::shared_ptr<>doppelt so viel Speicherplatz wie ein einfacher Zeiger benötigen, aber vor allem, weil sie schlechten Designs förderlich sind, bei denen es keine eindeutigen Eigentümer gibt. Dies führt wiederum zu einem Haarball von Objekten, die nicht zerstört werden können, weil sie gemeinsame Zeiger aufeinander halten.

Das beste Design ist dort, wo ein klares Eigentum festgelegt und hierarchisch ist, so dass im Idealfall überhaupt keine intelligenten Zeiger erforderlich sind. Wenn es beispielsweise eine Factory gibt, die eindeutige Objekte erstellt oder vorhandene zurückgibt, ist es sinnvoll, dass die Factory die von ihr erstellten Objekte besitzt und sie nur nach Wert in einem assoziativen Container (z. B. std::unordered_map) aufbewahrt, damit sie einfach zurückgegeben werden kann Zeiger oder Verweise auf seine Benutzer. Diese Factory muss eine Lebensdauer haben, die vor ihrem ersten Benutzer beginnt und nach ihrem letzten Benutzer endet (die hierarchische Eigenschaft), damit Benutzer keinen Zeiger auf ein bereits zerstörtes Objekt haben können.

Maxim Egorushkin
quelle
7
Beachten Sie, dass std :: auto_ptr ab C ++ 11 veraltet ist und ab C ++ 17 entfernt wird.
Sydius
" weil sie gemeinsame Zeiger zueinander halten " Nur wenn Sie einen schwerwiegenden Designfehler haben
neugieriger Kerl
6

Wenn Sie nicht möchten, dass der Angerufene von GimmeB () die Lebensdauer des Zeigers verlängern kann, indem Sie eine Kopie des ptr behalten, nachdem die Instanz von A gestorben ist, sollten Sie auf keinen Fall einen shared_ptr zurückgeben.

Wenn der Angerufene den zurückgegebenen Zeiger nicht für längere Zeit behalten soll, dh es besteht kein Risiko, dass die Lebensdauer von A vor der des Zeigers abläuft, ist ein roher Zeiger besser. Eine noch bessere Wahl ist jedoch die Verwendung einer Referenz, es sei denn, es gibt einen guten Grund, einen tatsächlichen Rohzeiger zu verwenden.

Und schließlich, falls der zurückgegebene Zeiger nach Ablauf der Lebensdauer der A-Instanz vorhanden sein kann, Sie aber nicht möchten, dass der Zeiger selbst die Lebensdauer des B verlängert, können Sie ein schwaches_ptr zurückgeben, das Sie zum Testen verwenden können ob es noch existiert.

Das Fazit ist, dass es normalerweise eine schönere Lösung gibt als die Verwendung eines Rohzeigers.

reko_t
quelle
3

Ich stimme Ihrer Meinung zu shared_ptr am besten verwendet wird, wenn Ressourcen explizit gemeinsam genutzt werden. Es stehen jedoch auch andere Arten von intelligenten Zeigern zur Verfügung.

In Ihrem genauen Fall: Warum nicht eine Referenz zurückgeben?

Ein Zeiger deutet darauf hin, dass die Daten möglicherweise null sind. Hier befindet sich jedoch immer eine Bin Ihrer A, sodass sie niemals null sein wird. Die Referenz bestätigt dieses Verhalten.

Davon abgesehen habe ich Leute gesehen, die die Verwendung von Anwendungen shared_ptrauch in nicht gemeinsam genutzten Umgebungen befürworteten und weak_ptrHandles gaben , mit der Idee, die Anwendung zu "sichern" und veraltete Zeiger zu vermeiden. Da Sie eine shared_ptrvon der wiederherstellen können weak_ptr(und dies ist die einzige Möglichkeit, die Daten tatsächlich zu manipulieren), ist dies leider immer noch ein gemeinsames Eigentum, auch wenn dies nicht beabsichtigt war.

Hinweis: Es gibt einen subtilen Fehler shared_ptr, bei dem eine Kopie von standardmäßig Adieselbe Bwie das Original hat, es sei denn, Sie schreiben explizit einen Kopierkonstruktor und einen Kopierzuweisungsoperator. Und natürlich würden Sie keinen rohen Zeiger verwenden A, um a zu halten B, oder :)?


Eine andere Frage ist natürlich, ob Sie dies tatsächlich tun müssen. Einer der Grundsätze für gutes Design ist die Kapselung . Um eine Kapselung zu erreichen:

Sie dürfen keine Griffe an Ihre Interna zurückgeben (siehe Gesetz von Demeter ).

Vielleicht ist die eigentliche Antwort auf Ihre Frage, dass anstatt eine Referenz oder einen Zeiger darauf zu verraten B, diese nur über Adie Benutzeroberfläche geändert werden sollte.

Matthieu M.
quelle
Ist Ihre Notiz ein subtiler Witz? Wenn ja, ist es zu subtil für mich, wenn nicht, dann ist es der gleiche Fehler wie mit einem
Rohzeiger
@stefaanv: Beachten Sie, dass in der OP-Frage das Datenelement nicht unbedingt dynamisch zugewiesen wird. Es muss nur dann dynamisch zugewiesen werden, wenn die shared_ptrLösung verwendet wird. Bei der Rückgabe einer Referenz kann es sich jedoch durchaus um ein reguläres Attribut handeln, und reguläre Attribute werden nicht flach kopiert.
Matthieu M.
@Matthieu, okay, dann ist mir die Notiz nicht wirklich klar. (Das Beispiel des OP zeigt deutlich die dynamische Zuordnung und seine Frage
betraf
@stefaanv: Das OP zeigt einen dynamischen Zeiger, also notwendigerweise eine dynamische Zuordnung und die Frage, von der ich glaube, ich weiß nur, von was ich zurückkehren soll gimmeB(obwohl die Antwort die innere Darstellung beeinflussen könnte). Ich werde versuchen, die Notiz zu klären.
Matthieu M.
Übrigens: +1 für Ihren Zusatz: Frage 1: Muss ich Interna wirklich aussetzen?
Stefaanv
2

Im Allgemeinen würde ich die Verwendung von rohen Zeigern so weit wie möglich vermeiden, da sie eine sehr zweideutige Bedeutung haben - Sie müssen möglicherweise die Zuordnung des Pointees aufheben, aber möglicherweise nicht, und nur von Menschen gelesene und geschriebene Dokumentationen sagen Ihnen, was der Fall ist. Und die Dokumentation ist immer schlecht, veraltet oder missverstanden.

Wenn der Besitz ein Problem darstellt, verwenden Sie einen intelligenten Zeiger. Wenn nicht, würde ich eine Referenz verwenden, wenn dies praktikabel ist.

Thiton
quelle
2
  1. Sie ordnen B bei der Konstruktion von A zu.
  2. Sie sagen, B sollte draußen nicht als Lebenszeit bestehen bleiben.
    Beide weisen darauf hin, dass B ein Mitglied von A ist und nur einen Referenz-Accessor zurückgibt. Überentwickeln Sie das?
Ricibob
quelle
2

Ich habe festgestellt, dass die C ++ - Kernrichtlinien einige sehr nützliche Hinweise für diese Frage enthalten:

Die Verwendung eines Rohzeigers (T *) oder eines intelligenteren Zeigers hängt davon ab, wem das Objekt gehört (dessen Verantwortung es ist, den Speicher des Objekts freizugeben).

besitzen :

smart pointer, owner<T*>

nicht besitzen:

T*, T&, span<>

Eigentümer <>, Bereich <> ist in der Microsoft GSL-Bibliothek definiert

Hier sind die Faustregeln:

1) Verwenden Sie niemals einen Rohzeiger (oder keine eigenen Typen), um den Besitz zu übergeben

2) Smart Pointer sollte nur verwendet werden, wenn eine Besitzersemantik beabsichtigt ist

3) T * oder Eigentümer bezeichnen ein einzelnes Objekt (nur)

4) Verwenden Sie Vektor / Array / Spanne für Array

5) Meines Wissens nach wird shared_ptr normalerweise verwendet, wenn Sie nicht wissen, wer das Objekt freigibt. Beispielsweise wird ein Objekt von einem Multithread verwendet

Camino
quelle
1

Es wird empfohlen, keine rohen Zeiger zu verwenden, aber Sie können nicht einfach alles durch ersetzen shared_ptr. In diesem Beispiel gehen Benutzer Ihrer Klasse davon aus, dass es in Ordnung ist, die Lebensdauer von B über die von A hinaus zu verlängern, und entscheiden sich möglicherweise aus eigenen Gründen, das zurückgegebene B-Objekt für einige Zeit zu halten. Sie sollten a zurückgeben weak_ptroder, wenn B bei Zerstörung von A absolut nicht existieren kann, einen Verweis auf B oder einfach einen Rohzeiger.

Hamstergen
quelle
Die Rückgabe von a weak_ptrlöst das Problem nicht wirklich. "Benutzer Ihrer Klasse gehen davon aus, dass es in Ordnung ist, die Lebensdauer von B zu verlängern", da sie den Befehl "soft_ptr" an dem Punkt sperren können, an dem A zerstört wird, und somit die Lebensdauer von B verlängern können. Es könnte sie jedoch dazu bringen, darüber nachzudenken, und natürlich kann B auch gehen, wenn sie es nicht verschlossen haben, wenn A zerstört wird.
Steve Jessop
0

Wenn Sie sagen: "Nehmen wir an, B ist eine Klasse, die semantisch nicht außerhalb der Lebensdauer von A existieren sollte."

Dies sagt mir, dass B logisch sein sollte nicht ohne A existieren , aber was ist mit physisch existierend? Wenn Sie sicher sein können, dass niemand versucht, ein * B nach A dtors zu verwenden, ist ein roher Zeiger möglicherweise in Ordnung. Andernfalls kann ein intelligenterer Zeiger angebracht sein.

Wenn Clients einen direkten Zeiger auf A haben, müssen Sie darauf vertrauen, dass sie damit angemessen umgehen. Versuchen Sie nicht, es zu speichern usw.

Seand
quelle