PIMPL idiom und std::auto_ptr - Destruktor wird nicht aufgerufen



  • Hallo liebe C++ community,
    ich habe offensichtlich ein kleines Verständnisproblem bezüglich der Nutzung eines auto_ptr in Zusammenhang mit dem PIMPL-idiom.

    code ist vereinfacht etwa so:

    // --- HEADER Test.h --
    #include <string>
    #include <memory>
    
    class TestImp;
    class Test {
    public:
    	Test (std::string filename);
    private:
    	Test (const Test & rhs){}
    	Test & operator=(const Test& rhs) {return *this;}	
    	std::auto_ptr<TestImp> imp;
    };
    
    // --- Test.cpp --
    #include "Tets.h"
    #include <iostream>
    class TestImp{
    public:
    	TestImp(std::string file): filename(file) {
    		std::cout<<"TestImp CTOR";
    	}
    	~TestImp() {std::cout<<"TestImp DTOR";}
    private:
    	std::string filename;
    };
    Test::Test(std::string file):image(new TestImp(file)) {}
    

    Das funktioniert wohl nicht, da TestImp für den auto_ptr nicht definiert ist und er somit nicht den Destruktor aufrufen kann. Würde ich anstelle des auto_ptr einen rohen Pointer verwenden und dann eben einen Destruktor für Test definieren - klappt die ganze Sache wunderbar.

    Nun ist meine Frage, wie ich das PIMPL-idiom in Zusammenhang mit smart-pointern umsetzen kann. Es ist ja schliesslich nicht Sinn und Zweck der ganzen Sache die Implementationsdetails anzugeben nur damit der smart-pointer den Destructor findet.

    Grüße



  • Es muss lediglich der Destruktor von Test non-inline definiert werden, damit std::auto_ptr TestImp ordnungsgemäß zerstören kann.



  • Ja, die Möglichkeit hab ich ja schon erwähnt.
    Allerdings find ich es nicht gerade elegant extra nen Destruktor anzugeben mit leerem body.

    Testweise hab ich mal smart-pointer aus boost verwendet:
    - scoped_ptr funktioniert so ebenfalls nicht,
    - shared_ptr hingegen schon.
    Nur ist der code der entsprechenden header-datei für mich extrem schwer zu lesen und zu verstehen. Habe leider nicht den Grund dafür gefunden.



  • pimplpompl schrieb:

    - shared_ptr hingegen schon.

    Das wundert mich jetzt aber. Hatte shared_ptr nicht die Einschränkung, dass der Typ immer bereits vollständig bekannt sein muss?



  • otze schrieb:

    pimplpompl schrieb:

    - shared_ptr hingegen schon.

    Das wundert mich jetzt aber. Hatte shared_ptr nicht die Einschränkung, dass der Typ immer bereits vollständig bekannt sein muss?

    Nein.

    boost schrieb:

    shared_ptr and most of its member functions place no requirements on T; it is allowed to be an incomplete type, or void.



  • Obwohl's typischerweise funktioniert mit std::auto_ptr, wenn man den Destruktor der "äußeren" Klasse da definiert, wo TestImp vollständig bekannt ist, ist dies nicht vom Standard garantiert. Laut Standard lässt auto_ptr nur Ts zu, die vollständig sind.

    In C++0x wird es std::unique_ptr geben. Hier gibt es diese Bedingung nicht, jedenfalls dann nicht, wenn man einen eigenen "Deleter" verwendet. Also...

    class TestImpl; // wird woanders definiert
    
    struct TestImplDeleter {
      void operator()(TestImpl*) const; // wird woanders definiert
    };
    
    class Test
    {
      std::unique_ptr<TestImpl,TestImplDeleter> pimpl;
    public:
      ...
    };
    

    wäre völlig in Ordnung.



  • krümelkacker schrieb:

    In C++0x wird es std::unique_ptr geben. Hier gibt es diese Bedingung nicht, jedenfalls dann nicht, wenn man einen eigenen "Deleter" verwendet.

    Super. Einen Deleter zu implementieren macht auch weniger Arbeit als die Angabe eines leeren Destruktors.



  • War das Ernst oder Sarkasmus, Shlo?



  • Ist es eine rhetorische Frage?

    Wenn der OP meint, einen Destruktor mit einem leeren Body zu implementieren, nicht elegant sei, wird er wohl kaum jedes Mal einen Deleter schreiben wollen.



  • Shlo schrieb:

    Ist es eine rhetorische Frage?

    Nein, die Frage war ernst gemeint.

    Shlo schrieb:

    Wenn der OP meint, einen Destruktor mit einem leeren Body zu implementieren, nicht elegant sei, wird er wohl kaum jedes Mal einen Deleter schreiben wollen.

    Ich habe nicht den Eindruck, dass Du meinen Beitrag überhaupt richtig gelesen hast. Mir ging es um Korrektheit, nicht um Eleganz.



  • Erstmal herzlichen Dank für eure Antworten.

    Ich habe erstmal wie vorgeschlagen die Variante ausprobiert, einen Destruktor zu definieren (Dekl. in header, Definition in cpp).
    So wird zwar der Destruktor aufgerufen, der compiler (VS2010) spuckt allerdings trotzdem eine Warnung (C4150) aus:

    1>...pfad...\vc\include\memory(931): warning C4150: Löschen eines Zeigers auf den nicht definierten Typ 'TestImp'. Destruktor wurde nicht aufgerufen.
    1>          ...pfad...\test.h(4): Siehe Deklaration von 'TestImp'
    1>          ...pfad...\vc\include\memory(930): Bei der Kompilierung der  Klassen-template der std::auto_ptr<_Ty>::~auto_ptr(void)-Memberfunktion
    1>          with
    1>          [
    1>              _Ty=TestImp
    1>          ]
    1>          ...pfad...\test.h(18): Siehe Verweis auf die Instanziierung der gerade kompilierten Klassen-template "std::auto_ptr<_Ty>".
    1>          with
    1>          [
    1>              _Ty=TestImp
    1>          ]
    

    Danach hab ich krümelkackers Vorschlag umgesetzt und nutze nun einen unique_ptr, womit dies alles auch problemlos funktioniert.
    Soweit erstmal ganz nett.
    Nun stelle ich mir allerdings trotzdem die Frage, wie das beim shared_ptr umgesetzt wurde, da dort wohl kaum C++0x features zum Einstz kommen.
    Hat diesbezüglich jemand noch eine Erklärung für mich parat?

    Danke im Vorraus und Grüße



  • Argh, mal wieder Voraus falsch geschrieben 🙂

    Noch eine Ergänzung.
    Die Variante mit dem unique-pointer macht natürlich irgendwie recht wenig Sinn im Zusammenhang mit dem PIMPL-idiom.
    Wenn ich ohnehin extra einen deleter angeben muss und diesen dann folgendermassen in der cpp definiere:

    void TestImplDeleter::operator()(TestImp* p) const {
    	delete p;
    }
    

    dann könnte ich natürlich die ganze Sache mit dem smart-pointer auch vergessen und tatsächlich einfach einen Destruktor für Test angeben, der ein delete auf dem pointer aufruft.



  • krümelkacker schrieb:

    Mir ging es um Korrektheit, nicht um Eleganz.

    Verstehe. Deshalb schlägst du die Verwendung von Klassen eines noch nicht fertiggestellten Standards vor.

    pimplpompl schrieb:

    Erstmal herzlichen Dank für eure Antworten.

    Ich habe erstmal wie vorgeschlagen die Variante ausprobiert, einen Destruktor zu definieren (Dekl. in header, Definition in cpp).
    So wird zwar der Destruktor aufgerufen, der compiler (VS2010) spuckt allerdings trotzdem eine Warnung (C4150) aus:

    Irgendwas machst du falsch. Bei folgendem Code wird sogar mit /W4 keine Warnung ausgegeben.

    // .h
    #include <memory>
    
    class Test // : boost::noncopyable
    {
    public:
        Test();
        ~Test();
    
    private:
        class TestImp;
        std::auto_ptr<TestImp> imp;
    };
    
    // .cpp
    class Test::TestImp
    {
    public:
        ~TestImp()
        {
        }
    };
    
    Test::Test()
        : imp(new TestImp)
    {
    }
    
    Test::~Test()
    {
    }
    


  • Stimmt - so funktionierts und es gibt keine Warnung.

    Das Problem mit der Warnung war wohl, dass ich in der Definition von Test den privaten CCTOR mit leerem body angegeben habe.

    Definiere ich den CCTOR in der cpp nun folgendermaßen

    Test::Test(const Test& rhs) : imp(new TestImp) {}
    

    funktioniert es problemlos.

    Wie handhabt ihr denn das PIMPL-idiom? Mit smart-pointern, mit rohen pointern, ohnehin selten PIMPL, falls smart-pointer dann wie obiges Beispiel durch definieren eines ggf. leeren DTORs?



  • Shlo schrieb:

    krümelkacker schrieb:

    Mir ging es um Korrektheit, nicht um Eleganz.

    Verstehe. Deshalb schlägst du die Verwendung von Klassen eines noch nicht fertiggestellten Standards vor.

    Das, was Du als Vorschlag interpretiert hast, war als Zusatzinformation gedacht. Die Hauptbotschaft war: auto_ptr kann streng genommen (nach dem C++ Standard) nicht das, was hier gefordert wird -- mit oder ohne "Inline-Destruktor". Diese Botschaft ist sowohl richtig als auch on-topic, auch wenn es kein Lösungsvorschlag war. Nicht jeder Beitrag muss in einem Diskussionsforum einen Lösungsvorschlag enthalten. Wir sind hier nicht bei Stackoverflow. So, wie ich das sehe, bleibt eigentlich nichts außer dem unfreundlichen Ton von Deiner "Kritik" übrig.

    pimplpompl schrieb:

    ...
    dann könnte ich natürlich die ganze Sache mit dem smart-pointer auch vergessen und tatsächlich einfach einen Destruktor für Test angeben, der ein delete auf dem pointer aufruft.

    So richtig zufriedenstellend ist das alles nicht. Da stimme ich Dir zu.

    pimplpompl schrieb:

    Wie handhabt ihr denn das PIMPL-idiom? Mit smart-pointern, mit rohen pointern, ohnehin selten PIMPL, falls smart-pointer dann wie obiges Beispiel durch definieren eines ggf. leeren DTORs?

    Ich benutze dieses Muster nicht so oft. Die paar Male, wo ich es eingesetzt habe, habe ich es mit rohen Zeigern gemacht. In C++0x würde ich wahrscheinlich wirklich unique_ptr mit eigenem Deleter verwenden. Damit spart man sich auch mehr als einem benutzerdefinierten Destruktor. Move-Ctor und Move-Assignment bekommt man dann auch gratis vom Compiler dazu. Das wär's mir also wert.



  • krümelkacker schrieb:

    Das, was Du als Vorschlag interpretiert hast, war als Zusatzinformation gedacht. Die Hauptbotschaft war: auto_ptr kann streng genommen (nach dem C++ Standard) nicht das, was hier gefordert wird -- mit oder ohne "Inline-Destruktor".

    Jetzt machst du mich aber neugierig: Erkläre doch mal, was du im diesem Kontext unter streng verstehst. So, wie ich das sehe, steckt hinter dieser Aussage nichts, außer Wichtigtuerei.

    krümelkacker schrieb:

    So, wie ich das sehe, bleibt eigentlich nichts außer dem unfreundlichen Ton von Deiner "Kritik" übrig.

    Das siehst du falsch.



  • Shlo schrieb:

    Jetzt machst du mich aber neugierig:

    Hmm... Dann hat es ja doch etwas gebracht, dass ich mich 3mal wiederholt habe.

    Shlo schrieb:

    Erkläre doch mal, was du im diesem Kontext unter streng verstehst.

    Siehe ersten Abschnitt meines ersten Beitrags. Für den Fall, dass Du meinen Worten aus was für Gründen auch immer keinen Glauben schenken kannst, folgen entsprechende Zitate...

    ISO C++ Standard (2003) §17.4 Library-wide requirements

    In certain cases (..., operations on types used to instantiate library template components), the C++ Standard Library depends on components supplied by a C++ program. If these components do not meet their requirements, the Standard places no requirements on the implementation.

    In particular, the effects are undefined in the following cases:

    • [...]
    • if an incomplete type is used as a template argument when instantiating a template component.

    Weiter macht der C++ Standard keine Ausnahme für auto_ptr.

    Der kommende C++ Standard enthält dagegen folgendes:

    n3225.pdf, §17.6 Library-wide requirements

    • [...]
    • if an incomplete type is used as a template argument when instantiating a template component unless specifically allowed for that component.

    n3225.pdf, §20.9.9 Class template unique_ptr

    ...The template parameter T of unique_ptr may be an incomplete type...

    Ja, das ist Korintenkackerrei, gerade weil es ja auch ohne diese Garantie mit auto_ptr zu funktionieren scheint (wenn man's "richtig" macht) ... aber streng genommen (nach ISO C++ Standard) ruft es undefiniertes Verhalten hervor. Ich halte es auf jeden Fall für erwähnenswert und ich verstehe nicht ganz, warum ich mich deswegen Dir gegenüber zig mal rechtfertigen soll.

    Shlo schrieb:

    So, wie ich das sehe, steckt hinter dieser Aussage nichts, außer Wichtigtuerei.

    krümelkacker schrieb:

    So, wie ich das sehe, bleibt eigentlich nichts außer dem unfreundlichen Ton von Deiner Kritik übrig.

    Das siehst du falsch.

    Let's agree to disagree.

    kk



  • Ich nehme solche Hinweise wie den von krümelkacker immer dankend an.
    Es gibt doch viele Beispiele von code, die eigentlich undefiniertes Verhalten produzieren, in der Praxis aber dennoch (teilweise durch kleine tricks) funktionieren. Nichts desto trotz vermeidet man das doch lieber.

    Selbst wenn man es nicht vermeiden möchte (weil es ja anscheinend trotzdem funktioniert) ist es sicherlich hilfreich die Hintergründe zu kennen und genau zu wissen, was man da wie anstellt.

    Insofern kann ich krümelkacker nur danken für die Informationen bezüglich momentanem Standard und Ausblick auf Zukünftiges.
    So wurden in diesem thread nicht nur Lösungen präsentiert sondern auch Informationen bezüglich der Standardkonformität.
    Gerade solche Zusatzinformationen machen doch einen solchen thread auch für anderes Forenuser interessant.

    Grüsse.



  • krümelkacker schrieb:

    Ja, das ist Korintenkackerrei, gerade weil es ja auch ohne diese Garantie mit auto_ptr zu funktionieren scheint (wenn man's "richtig" macht) ... aber streng genommen (nach ISO C++ Standard) ruft es undefiniertes Verhalten hervor. Ich halte es auf jeden Fall für erwähnenswert und ich verstehe nicht ganz, warum ich mich deswegen Dir gegenüber zig mal rechtfertigen soll.

    Nun, auch wenn du die Aussage noch weitere zehnmal wiederholst, wird sie nicht richtig. Man sollte nämlich wissen, wann ein Typ unvollständig ist, bevor man die zitierten Stellen aus dem Standard ins Spiel bringt. Denn durch die Definition eines non-inline Destruktors in Test, ist TestImp bei Instantiierung von std::auto_ptr<TestImp> (genauer gesagt dessen Destruktors) nicht mehr unvollständig. Wozu würde man sonst einen Destruktor mit leerem Body definieren?

    Ob streng genommen oder nicht, lagst du daneben. Schade, dass du nicht selbst darauf gekommen bist.


  • Mod

    Shlo schrieb:

    krümelkacker schrieb:

    Ja, das ist Korintenkackerrei, gerade weil es ja auch ohne diese Garantie mit auto_ptr zu funktionieren scheint (wenn man's "richtig" macht) ... aber streng genommen (nach ISO C++ Standard) ruft es undefiniertes Verhalten hervor. Ich halte es auf jeden Fall für erwähnenswert und ich verstehe nicht ganz, warum ich mich deswegen Dir gegenüber zig mal rechtfertigen soll.

    Nun, auch wenn du die Aussage noch weitere zehnmal wiederholst, wird sie nicht richtig. Man sollte nämlich wissen, wann ein Typ unvollständig ist, bevor man die zitierten Stellen aus dem Standard ins Spiel bringt. Denn durch die Definition eines non-inline Destruktors in Test, ist TestImp bei Instantiierung von std::auto_ptr<TestImp> (genauer gesagt dessen Destruktors) nicht mehr unvollständig. Wozu würde man sonst einen Destruktor mit leerem Body definieren?

    Ob streng genommen oder nicht, lagst du daneben. Schade, dass du nicht selbst darauf gekommen bist.

    Unfug.


Anmelden zum Antworten