shared_ptr magic :)

88

Herr Lidström und ich hatten einen Streit :)

Die Behauptung von Herrn Lidström ist, dass für ein Konstrukt shared_ptr<Base> p(new Derived);Base keinen virtuellen Destruktor benötigt:

Armen Tsirunyan : "Wirklich? Wird der shared_ptr korrekt bereinigt? Könnten Sie bitte in diesem Fall zeigen, wie dieser Effekt implementiert werden könnte?"

Daniel Lidström : "Der shared_ptr verwendet einen eigenen Destruktor, um die Concrete-Instanz zu löschen. Dies wird in der C ++ - Community als RAII bezeichnet. Mein Rat ist, dass Sie alles über RAII lernen. Dies erleichtert Ihre C ++ - Codierung bei der Verwendung erheblich RAII in allen Situationen. "

Armen Tsirunyan : "Ich weiß über RAII Bescheid, und ich weiß auch, dass der Destruktor shared_ptr möglicherweise den gespeicherten px löscht, wenn pn 0 erreicht. Wenn px jedoch einen statischen Typzeiger auf Baseund einen dynamischen Typzeiger auf hat Derived, dann, sofern Basekein virtueller Destruktor vorhanden ist führt zu undefiniertem Verhalten. Korrigieren Sie mich, wenn ich falsch liege. "

Daniel Lidström : "Der shared_ptr weiß, dass der statische Typ Concrete ist. Er weiß das, seit ich ihn in seinem Konstruktor übergeben habe! Scheint ein bisschen magisch, aber ich kann Ihnen versichern, dass er beabsichtigt und äußerst nett ist."

Also, beurteilen Sie uns. Wie ist es möglich (wenn ja ), shared_ptr zu implementieren, ohne dass polymorphe Klassen einen virtuellen Destruktor haben müssen? Danke im Voraus

Armen Tsirunyan
quelle
3
Du hättest auf den ursprünglichen Thread verlinken können .
Darin Dimitrov
8
Eine andere interessante Sache ist, dass shared_ptr<void> p(new Derived)das DerivedObjekt auch durch seinen Destruktor zerstört wird, unabhängig davon, ob es ist virtualoder nicht.
Dale
7
Tolle Art, eine Frage zu stellen :)
Rubenvb
5
Obwohl shared_ptr dies zulässt, ist es eine wirklich schlechte Idee , eine Klasse als Basis ohne virtuellen dtor zu entwerfen. Daniels Kommentare zu RAII sind irreführend - es hat nichts damit zu tun -, aber das zitierte Gespräch klingt nach einer einfachen Fehlkommunikation (und einer falschen Annahme, wie shared_ptr funktioniert).
6
Nicht RAII, sondern löscht den Destruktor. Sie müssen vorsichtig sein, denn shared_ptr<T>( (T*)new U() )wo struct U:Twird nicht das Richtige getan (und dies kann indirekt leicht getan werden, wie eine Funktion, die a übernimmt T*und a übergeben wird U*)
Yakk - Adam Nevraumont

Antworten:

73

Ja, es ist möglich, shared_ptr auf diese Weise zu implementieren. Boost funktioniert und der C ++ 11-Standard erfordert dieses Verhalten ebenfalls. Als zusätzliche Flexibilität verwaltet shared_ptr mehr als nur einen Referenzzähler. Ein sogenannter Deleter wird normalerweise in denselben Speicherblock gestellt, der auch die Referenzzähler enthält. Der lustige Teil ist jedoch, dass der Typ dieses Löschers nicht Teil des Typs shared_ptr ist. Dies wird als "Typlöschung" bezeichnet und ist im Grunde die gleiche Technik, die zum Implementieren der Boost :: -Funktion "polymorpher Funktionen" oder der std :: -Funktion zum Ausblenden des tatsächlichen Funktortyps verwendet wird. Damit Ihr Beispiel funktioniert, benötigen wir einen Konstruktor mit Vorlagen:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Wenn Sie dies also mit Ihren Klassen Base und Derived verwenden ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

... wird der Vorlagenkonstruktor mit Y = Derived verwendet, um das shared_ptr-Objekt zu erstellen. Der Konstruktor hat somit die Möglichkeit, die entsprechenden Löschobjekt- und Referenzzähler zu erstellen und einen Zeiger auf diesen Steuerblock als Datenelement zu speichern. Wenn der Referenzzähler Null erreicht, wird der zuvor erstellte und abgeleitete Deleter zum Entsorgen des Objekts verwendet.

Der C ++ 11-Standard hat Folgendes zu diesem Konstruktor (20.7.2.2.1) zu sagen:

Benötigt: p muss konvertierbar sein zu T*. Ymuss ein vollständiger Typ sein. Der Ausdruck delete pmuss gut geformt sein, ein genau definiertes Verhalten aufweisen und keine Ausnahmen auslösen.

Effekte: Konstruiert ein shared_ptrObjekt , das besitzt den Zeiger p.

Und für den Destruktor (20.7.2.2.2):

Auswirkungen: Wenn *thises leer ist oder das Eigentum mit einer anderen shared_ptrInstanz teilt ( use_count() > 1), treten keine Nebenwirkungen auf. Andernfalls, wenn *thisein Objekt besitzt pund eine deleter d, d(p)genannt wird. Andernfalls *thisbesitzt if einen Zeiger pund delete pwird aufgerufen.

(Die Betonung mit Fettschrift liegt bei mir).

sellibitze
quelle
the upcoming standard also requires this behaviour: (a) Welcher Standard und (b) können Sie bitte einen Verweis (auf den Standard) geben?
Kevinarpe
Ich möchte nur einen Kommentar zu @sellibitzes Antwort hinzufügen, da ich nicht genug Punkte dazu habe add a comment. IMO, es ist mehr Boost does thisals the Standard requires. Ich glaube nicht, dass der Standard dies nach meinem Verständnis erfordert. Talking about @sellibitze ‚s Beispiel shared_ptr<Base> sp (new Derived);, erfordert von constructorfragen nur für delete Derivedsein gut definiert und gut ausgebildet. Für die Spezifikation von destructorgibt es auch eine p, aber ich denke nicht, dass sie sich auf die pin der Spezifikation von bezieht constructor.
Lujun Weng
28

Wenn shared_ptr erstellt wird, speichert es ein Löscherobjekt in sich. Dieses Objekt wird aufgerufen, wenn shared_ptr die freigegebene Ressource freigeben soll. Da Sie wissen, wie Sie die Ressource zum Zeitpunkt der Erstellung zerstören, können Sie shared_ptr mit unvollständigen Typen verwenden. Wer auch immer den shared_ptr erstellt hat, hat dort einen korrekten Löscher gespeichert.

Sie können beispielsweise einen benutzerdefinierten Löscher erstellen:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p ruft DeleteDerived auf, um das spitze Objekt zu zerstören. Die Implementierung erledigt dies automatisch.

Yakov Galka
quelle
4
+1 für die Bemerkung zu unvollständigen Typen, sehr praktisch bei Verwendung von a shared_ptrals Attribut.
Matthieu M.
16

Einfach,

shared_ptr verwendet eine spezielle Löschfunktion, die vom Konstruktor erstellt wird, der immer den Destruktor des angegebenen Objekts und nicht den Destruktor von Base verwendet. Dies ist ein wenig Arbeit mit der Vorlagen-Metaprogrammierung, funktioniert aber.

Sowas in der Art

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}
Artyom
quelle
1
hmm ... interessant, ich
fange
1
@Armen Tsirunyan Sie sollten einen Blick in die Designbeschreibung des shared_ptr werfen, bevor Sie mit der Diskussion beginnen. Diese 'Erfassung des Deleters' ist eines der wesentlichen Merkmale von shared_ptr ...
Paul Michalik
6
@ paul_71: Ich stimme dir zu. Andererseits glaube ich, dass diese Diskussion nicht nur für mich nützlich war, sondern auch für andere Leute, die diese Tatsache über shared_ptr nicht kannten. Ich denke, es war sowieso keine große Sünde, diesen Thread zu starten :)
Armen Tsirunyan
3
@Armen Natürlich nicht. Vielmehr haben Sie gute Arbeit geleistet, indem Sie auf diese wirklich sehr wichtige Funktion von shared_ptr <T> hingewiesen haben, die selbst von erfahrenen C ++ - Entwicklern häufig überwacht wird.
Paul Michalik