Frage shared_ptr magic :)


Mr. Lidström und ich hatten einen Streit :)

Herr Lidströms Behauptung ist, dass ein Konstrukt shared_ptr<Base> p(new Derived); benötigt Base nicht, um einen virtuellen Destruktor zu haben:

Armen Tsirunyan: "Wirklich? Wird die shared_ptr sauber aufräumen? Könnten Sie bitte in diesem Fall demonstrieren, wie dieser Effekt umgesetzt werden könnte? "

Daniel Lidström: "Das 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. Es wird Ihre C ++ - Programmierung so viel einfacher machen, wenn Sie RAII in allen Situationen verwenden. "

Armen Tsirunyan: "Ich weiß über RAII, und ich weiß auch, dass schließlich die shared_ptr Destruktor kann den gespeicherten px löschen, wenn pn 0 erreicht. Aber wenn px einen statischen Typ-Zeiger auf hat Base und dynamischer Typ Zeiger auf Deriveddann, es sei denn Base hat einen virtuellen Destruktor, dies führt zu undefiniertem Verhalten. Korrigiere mich, wenn ich falsch liege. "

Daniel Lidström: "Das shared_ptr weiß, dass der statische Typ Beton ist. Es weiß das, seit ich es in seinem Konstruktor übergeben habe! Scheint ein wenig wie Magie, aber ich kann Ihnen versichern, dass es von Design und extrem nett ist. "

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


76
2017-10-10 09:41


Ursprung


Antworten:


Ja, es ist möglich, shared_ptr auf diese Weise zu implementieren. Boost tut das und der C ++ 11-Standard erfordert auch dieses Verhalten. Als zusätzliche Flexibilität verwaltet shared_ptr mehr als nur einen Referenzzähler. Ein sogenannter Löscher wird normalerweise in denselben Speicherblock gesetzt, der auch die Referenzzähler enthält. Der spaßige Teil ist jedoch, dass der Typ dieses Deleters nicht Teil des Typs shared_ptr ist. Dies wird als "Typ löschen" bezeichnet und ist im Grunde die gleiche Technik, die zum Implementieren der "polymorphen Funktionen" boost :: function oder std :: function zum Verbergen des tatsächlichen Funktortyps verwendet wird. Damit Ihr Beispiel funktionieren kann, benötigen wir einen Template-Konstruktor:

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

Also, wenn Sie dies mit Ihren Klassen Base und Derived ...

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

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

... wird der Konstruktor mit Y = Derived verwendet, um das Objekt shared_ptr zu konstruieren. 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 auf Ableitungen bezogene Deleter verwendet, um das Objekt zu beseitigen.

Der C ++ 11 Standard sagt zu diesem Konstruktor (20.7.2.2.1) Folgendes aus:

Benötigt:  p muss umwandelbar sein T*. Y soll ein vollständiger Typ sein. Der Ausdruck delete p soll wohlgeformt sein, wohldefiniertes Verhalten haben und keine Ausnahmen werfen.

Auswirkungen: Konstruiert a shared_ptr Objekt Das besitzt der Zeiger p.

...

Und für den Destruktor (20.7.2.2.2):

Auswirkungen: Ob *this ist leer oder Aktienbesitz mit einem anderen shared_ptr Instanz (use_count() > 1), es gibt keine Nebenwirkungen.   Ansonsten, wenn *this besitzt ein Objekt p und ein Deleter d, d(p) wird genannt.    Ansonsten, wenn *this besitzt einen Zeiger p, und delete p wird genannt.

(Hervorhebung mit Fettschrift gehört mir).


67
2017-10-10 11:18



Wenn shared_ptr erstellt wird, speichert es ein Löscher Objekt in sich selbst. Dieses Objekt wird aufgerufen, wenn das shared_ptr die angegebene Ressource freigibt. Da Sie wissen, wie Sie die Ressource zum Zeitpunkt der Konstruktion zerstören können, können Sie shared_ptr mit unvollständigen Typen verwenden. Wer auch immer shared_ptr erstellt hat, hat dort einen korrekten Deleter gespeichert.

Sie können beispielsweise einen benutzerdefinierten Deleter 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 macht dies automatisch.


26
2017-10-10 09:47



Einfach,

shared_ptr verwendet eine spezielle Löschfunktion, die vom Konstruktor erstellt wird, der immer verwendet wird der Destruktor des gegebenen Objekts und nicht der Destruktor von Base, das ist ein bisschen Arbeit mit Template-Meta-Programmierung, aber es funktioniert.

So ähnlich

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}

14
2017-10-10 09:46