Pointer-to-impl und smart ptr



  • Kommt man eigentlich nicht um das leere Definieren des Destruktors bei bekanntem Typen, wenn man eine sichere Freigabe erzwingen will?

    Fehlt hier ein "herum"? 😉

    Einen leeren dtor muss man nicht definieren, der Compiler-generierte macht genau dasselbe.



  • hustbaer schrieb:

    Fehlt hier ein "herum"? 😉

    Tatsächlich, hehe. Ich sollte nach mehrmaligem Umformulieren vielleicht etwas genauer lesen... 🙂

    hustbaer schrieb:

    Einen leeren ctor muss man nicht definieren, der Compiler-generierte macht genau dasselbe.

    Ich dachte, die compilergenerierten Konstruktoren und Destruktoren würden leeren Funktionen gleichkommen, die inline innerhalb der Klassendefinition definiert würden. Im Header ist der Typ des Smart-Pointers (bei meinem Beispiel T ) allerdings noch nicht vollständig bekannt, wodurch checked_delete scheitert. In der Klassen-Implementierungsdatei (bei mir MyClass.cpp ) hat man ein #include "T.hpp" , das durch die Definition von T ermöglicht, im Destruktor der Klasse den Smart-Pointer richtig zu zerstören.



  • Hihi, und ich hab "ctor" statt "dtor" geschrieben, peinlich 🙂 (habs korrigiert)

    OK, ich habe deine Frage falsch verstanden. Genauer gesagt: das "bei bekanntem Typen".

    Ja, in dem Fall macht es einen Unterschied. Netterweise verwendet da die Boost ja das checked_delete, was diesen Fehler von einem blöd zu findenden Runtime-Fehler in einen nicht zu übersehenden Compiletime-Fehler verwandelt 🙂

    Aber wie schon erwähnt kann man sich ja selbst einen Smart-Pointer basteln, der korrekt zerstört, sich aber ansonsten wie scoped_ptr verhält (und abgesehen von einem zusätzlichen Funktionszeiger auch kaum Overhead hat).



  • wenn Ihr das kopieren beim auto_ptr verhindern wollt, macht das ding doch const !
    beim Pimpl erzeugst die Impl klasse doch meist sowieso in der init Liste oder ???

    das mit dem Compilerfehler versus Warnung seh ich auch ned so tragisch wenn ich das new vergesse, gibts den crash(neben der Warnung) spaetestens zur laufzeit mit ner symantisch richtigen NULL referenz Meldung. Vergess ich das new in der initliste aber ned, krieg ich da ja scho die compilerfehler um die ohren geworfen, wenn meine klasse unvollstaendig definiert ist ...

    Ciao ...


  • Mod

    Die Definition des Destruktor der Öffentlichkeit (=dem Compiler) zu überlassen, läuft dem pimpl-Idiom zuwider, soll doch durch dieses erreicht werden, dass Implementationsdetails unsichtbar werden. Ein öffentlicher definierter Destruktor bewirkt das Gegenteil. Gleiches gilt im Grunde für Smartpointer für den pimpl, auch hier wird letztlich die Implementation eingeengt:
    beispielsweise ist es denkbar, dass eine Implementationsklasse nicht mehr Speicher benötigt als ein einzelner Zeiger - in diesem Falle könnte man den Speicher, den der pimpl-Zeiger nach außen hin belegt, direkt wiederverwenden. In solchen Fällen wäre sogar denkbar, dass die vom Compiler generierten Copyctor/Zuweiungsoperator/Destruktor das richtig tun würden - ab dies bedeutet letztlich das Offenlegen von Implementationsdetails (konsequent zu Ende gedacht bedeutet das sogar, dass pimpl korrekterweise per void* implementiert werden sollte).
    Steht einmal fest, diese Member sowieso implementiert werden müssen, ist der Nutzen eines Smartpointers praktisch null. Selbst das Argument der Exceptionsicherheit zieht bei einem einzelnen Member nicht. In Hinblick auf die Schwierigkeiten, die mit unvollständigen Typen und Smartpointern verbunden sind, sehe ich wirklich keinen Vorteil darin, diese auf Biegen und Brechen einsetzen zu wollen.



  • Ich glaub ich steig hier ned wirklich durch ...
    Was ist denn nun das eigentliche Problem ???

    ab dies bedeutet letztlich das Offenlegen von Implementationsdetails

    Es wird doch nix offen gelegt .... die einzige anforderung die zumindest der auto_ptr hat, ist das der Destruktor dem auto_ptr "zugaenglich" ist.

    Man koennt auch den destruktor in der Impl Klasse privat machen, aber die friend deklaration aufn auto_ptr sieht sicher schrecklich aus, und erreicht auch nix wirklich, weil jeder ja nen auto_ptr anlegen koennte. Um die verwendung der klasse einzuschraenken, muesst man sich was anderes einfallen lassen. Aber das ist doch kein problem ?
    Ne Impl klasse ist doch meist total versteckt in ner lib, ohne das man an deren header rankommt. wieso sollte ich die extra noch mal schuetzen ??? wieso sollte ich dann einen nicht public destruktor brauchen koennen ??? das leuchtet mir ned ein ...

    Die Definition des Destruktor der Öffentlichkeit (=dem Compiler) zu überlassen

    versteh ich auch ned ... der auto_ptr funktioniert doch auch mit dem Compilergenerierten ???
    Also bei mir laesst sich Code generieren, wo in der Impl klasse kein Destruktor definiert ist ! also das Ding in nen auto_ptr packen, in den header zur auto_ptr deklaration den Forward packen, und in der cpp, den vollstaendigen header inkludieren, laesst sich wunderbar compilieren.
    Compiler ist VS 2005 ... ist das kein Standardverhalten ???

    Also ich muss den DTor weder expliziet definieren, noch muss ich die klasse kennen, nur die typen muessen bekannt sein (Forward). Ich seh das problem ned ...

    Ciao ....


  • Mod

    RHBaum schrieb:

    Also ich muss den DTor weder expliziet definieren, noch muss ich die klasse kennen, nur die typen muessen bekannt sein (Forward). Ich seh das problem ned ...

    Du weisst, was passiert - oder besser: nicht passiert - wenn delete auf einen Zeiger angewendet wird, dessen Pointeetyp unvollständig definiert ist?



  • Aah ich hab schon zu lang hier nicht mehr reingeschaut. Ich nehme noch kurz hierzu Stellung:

    Nexus schrieb:

    Wenn du scoped_ptr verwendest, geht das hier (sinngemäss) nicht?

    // MyClass.hpp
    #include <boost/scoped_ptr.hpp>
    
    class T;
    class MyClass
    {
        public:
            MyClass();
            ~MyClass();
        private:
            boost::scoped_ptr<T> Ptr;
    };
    
    // MyClass.cpp
    #include "MyClass.hpp"
    #include "T.hpp"        // Definition von T
    
    MyClass::MyClass()
    : Ptr(new T())
    {
    }
    
    MyClass::~MyClass() // T ist im Destruktor vollständig bekannt
    {
    }
    

    Doch, das geht genauso wie beim auto_ptr. Ich ziehe hier dann lediglich den std pointer vor, weil der scoped_ptr nichts besser macht, was für mich relevant wäre (die Warnung statt Fehler ist mir auch deutlich genug).

    camper schrieb:

    Steht einmal fest, diese Member sowieso implementiert werden müssen, ist der Nutzen eines Smartpointers praktisch null. Selbst das Argument der Exceptionsicherheit zieht bei einem einzelnen Member nicht. In Hinblick auf die Schwierigkeiten, die mit unvollständigen Typen und Smartpointern verbunden sind, sehe ich wirklich keinen Vorteil darin, diese auf Biegen und Brechen einsetzen zu wollen.

    Da stimme ich nicht zu. Es ist eine Prinzipsache, für Member smart pointer zu benutzen und ich weiche nur in Ausnahmefällen davon ab. Es ist nicht auf "Biegen und Brechen", denn es ist kein Mehraufwand vorhanden gegenüber einem rohen Zeiger. Ich sehe auch keine großartigen Schwierigkeiten, es ist jetzt klar, dass wenn ich den Destruktor selber definiere (was ich bei einem rohen Zeiger auch müsste), alles funktioniert. Die Ersparnis durch den smart pointer ist zugegebenermaßen gering, aber wenn es sich nicht ändern lässt, kann ich dagegen auch nichts machen.

    Das ganze Idiom ist halt ne Krücke, wenn es jemand grundsätzlich ablehnt, verstehe ich das sogar. Ansonsten gehört es IMHO schon zum guten Ton, ein automatisches Objekt zu verwenden, außer es bereitet wirklich Extra-Aufwand.



  • Vor einiger Zeit habe ich mir eine Smart-Pointer-Klasse implementiert, die ihr Pointee automatisch kopiert. Das scheint mir relativ geeignet für Pimpl zu sein (das Thema gabs auch mal im Forum).

    Zumindest habe ich es mit einer Indirektion über Funktionszeiger geschafft, durch eine Pointee-Deklaration (keine Definition) im Header und ohne Implementierung der Big Three das gewünschte Verhalten zu erzielen. Ich bin mir nicht sicher, ob das auf jedem Compiler geht (ich benutze MSVC++). Für die Interessierten ist hier der wichtige Teil meiner Implementierung zu sehen, ich habe einiges rausgekürzt. Leider bin ich noch nicht gross zum Testen gekommen, in bisherigen Fällen wurde aber immer korrekt freigegeben. Ich wäre sehr froh um Kritik und Ratschläge! Ich bin danach auch gerne bereit, meine komplette Implementierung zu zeigen, aber der momentane Code ist schon recht gross.

    Freie Funktionen zur Unterstützung:

    // Stellt einen vollständig definierten Typen T sicher, damit dessen Destruktor
    // aufgerufen werden kann. Wendet delete auf den übergebenen Zeiger an.
    template <typename T>
    void checked_delete(const T* pointer)
    {
    	sizeof(T);		// T muss vollständig definiert sein
    	delete pointer;
    }
    
    // Klasse, die ein Objekt klont, indem dessen Kopierkonstruktor aufgerufen wird.
    // Bei einem Nullzeiger als Argument wird ebenfalls ein Nullzeiger zurückgegeben. 
    template <typename T>
    T* default_cloner(const T* origin)
    {
    	return (origin == 0) ? 0 : new T(*origin);
    }
    
    // Klasse, die ein Objekt zerstört, indem delete angewandt wird.
    template <typename T>
    void default_deleter(T* pointer)
    {
    	checked_delete(pointer);
    }
    
    // Tut nichts (wird für Funktionszeiger, deren Aufruf nichts bewirken soll und keinen
    // vollständigen Typen erfodert, benötigt).
    template <typename T>
    void empty_deleter(T*)
    {
    }
    
    // Tut nichts (wird für Funktionszeiger, deren Aufruf nichts bewirken soll und keinen
    // vollständigen Typen erfodert, benötigt). Gibt immer einen Nullzeiger zurück.
    template <typename T>
    T* empty_cloner(const T*)
    {
    	return 0;
    }
    

    Klassendefinition mit einem Teil der Methoden:

    // Klassentemplate owned_ptr: Smart-Pointer, der sein Objekt im Destruktor automatisch
    // freigibt und im Kopierkonstruktor und Zuweisungsoperator kopiert.
    // T ist der Typ, der verwaltet werden soll.
    template <typename T>
    class owned_ptr
    {
    	public:
    		typedef void (*deleter_type)(T*);
    		typedef T*	 (*cloner_type) (const T*);
    
    	public:
    		// Standardkonstruktor: Setzt den Zeiger auf Null.
    		owned_ptr();
    
    		// Konstruktor: Übernimmt Herrschaft über den übergebenen Zeiger und initialisiert
    		// Deleter und Cloner.
    		explicit owned_ptr(T* new_pointer, deleter_type deleter = default_deleter, 
    			cloner_type cloner = default_cloner);
    
    		// Kopierkonstruktor: Kopiert die Instanz des übergebenen Smart-Pointers, falls
    		// dieser nicht auf Null zeigt. Wirft bei Fehlschlag der Kopie eine Exception.
    		owned_ptr(const owned_ptr& origin);
    
    		// Destruktor: Gibt Speicher des Zeigers mittels Deleter frei.
    		~owned_ptr();
    
    		// Zuweisungsoperator: Kopiert die Instanz des übergebenen Smart-Pointers und gibt
    		// den momentanen Inhalt von *this frei.
    		owned_ptr& operator= (const owned_ptr& origin);
    
    		// Tauscht Objekt-Zeiger, Deleter und Cloner zweier owned_ptr-Instanzen.
    		void swap(owned_ptr& other);
    
    		// Gibt den Speicher mittels Deleter frei und setzt den Zeiger auf Null zurück.
    		void reset();
    
    		// Gibt den Speicher frei und setzt den neuen Zeiger.
    		void reset(T* new_pointer, deleter_type deleter = default_deleter,
    			cloner_type cloner = default_cloner);
    
    		// Gibt Speicher frei und kopiert die Instanz des übergebenen owned_pts.
    		// Wirft eine Exception, falls die Kopie fehlschlägt.
    		void reset(const owned_ptr<T>& origin);
    
    	private:
    		// interner Zeiger auf Objekt
    		T* m_ptr;
    
    		// Zeiger auf Freigabefunktion
    		deleter_type m_deleter;
    
    		// Zeiger auf Klonfunktion
    		cloner_type m_cloner;
    };
    

    Implementierung der Methoden (ja, ich mag die Trennung auch bei Templates, auch wenn ich nicht immer ganz konsequent bin):

    #include <algorithm>
    
    template <typename T>
    owned_ptr<T>::owned_ptr()
    : m_ptr(0)
    , m_deleter(empty_deleter)
    , m_cloner(empty_cloner)
    {
    }
    
    template <typename T>
    owned_ptr<T>::owned_ptr(T* new_pointer, deleter_type deleter, cloner_type cloner)
    : m_ptr(new_pointer)
    , m_deleter(deleter)
    , m_cloner(cloner)
    {
    }
    
    template <typename T>
    owned_ptr<T>::owned_ptr(const owned_ptr<T>& origin)
    : m_ptr(origin.m_cloner(origin.m_ptr))
    , m_deleter(origin.m_deleter)
    , m_cloner(origin.m_cloner)
    {
    }
    
    template <typename T>
    owned_ptr<T>::~owned_ptr()
    {
    	(this->m_deleter)(m_ptr);
    }
    
    template <typename T>
    owned_ptr<T>& owned_ptr<T>::operator= (const owned_ptr<T>& origin)
    {
    	reset(origin);
    	return *this;
    }
    
    template <typename T>
    void owned_ptr<T>::swap(owned_ptr<T>& other)
    {
    	std::swap(m_ptr,	 other.m_ptr);
    	std::swap(m_deleter, other.m_deleter);
    	std::swap(m_cloner,	 other.m_cloner);
    }
    
    template <typename T>
    void owned_ptr<T>::reset()
    {
    	owned_ptr tmp;
    	swap(tmp);
    }
    
    template <typename T>
    void owned_ptr<T>::reset(T* new_pointer, deleter_type deleter, cloner_type cloner)
    {
    	owned_ptr tmp(new_pointer, deleter, cloner);
    	swap(tmp);
    }
    
    template <typename T>
    void owned_ptr<T>::reset(const owned_ptr<T>& origin)
    {
    	owned_ptr tmp(origin);
    	swap(tmp);
    }
    

  • Mod

    Optimizer schrieb:

    Doch, das geht genauso wie beim auto_ptr. Ich ziehe hier dann lediglich den std pointer vor, weil der scoped_ptr nichts besser macht, was für mich relevant wäre (die Warnung statt Fehler ist mir auch deutlich genug).

    Wobei weiterhin zu beachten ist, dass die Verwendung von auto_ptr zu undefiniertem Verhalten führt. Was jeder persönlich macht, ist sein Eigene Sache, aber in diesemForum weise ich nochmal darauf hin.

    Optimizer schrieb:

    Da stimme ich nicht zu. Es ist eine Prinzipsache, für Member smart pointer zu benutzen und ich weiche nur in Ausnahmefällen davon ab. Es ist nicht auf "Biegen und Brechen", denn es ist kein Mehraufwand vorhanden gegenüber einem rohen Zeiger. Ich sehe auch keine großartigen Schwierigkeiten, es ist jetzt klar, dass wenn ich den Destruktor selber definiere (was ich bei einem rohen Zeiger auch müsste), alles funktioniert. Die Ersparnis durch den smart pointer ist zugegebenermaßen gering, aber wenn es sich nicht ändern lässt, kann ich dagegen auch nichts machen.

    Auf "Prinzipien" reagiere ich grundsätzlich allergisch, weil das inhaltsleer ist. Letztlich liegt die Begründung dafür, wie der Code geschrieben wird dann nicht mehr in diesem Code selbst, sondern in der Person, die ihn geschrieben hat. Kein Idiom oder Konstrukt ist Selbstzweck.
    Es ist jetzt klar, dass Destruktor (und Copyctor/Copy-Zuweisung, falls benötigt) selbst implementiert werden müssen trotz der Verwendung von Smartpointern. Das ist in meinen Augen eine extrem negative Bilanz und nicht etwa ein "geringer Vorteil", weil es unnötig Aufmerksamkeit erfordert. An einem schlechten Tag hat man all diese Zusammenhänge auch schon mal wieder vergessen, schaut sich den Destruktor an, der scheinbar leer ist, und ersetzt ihn wieder durch den vom Compiler generierten (möglich jedenfalls bei auto_ptr oder smart_ptr ohne zu Compilerfehlern zu führen). Bei einem rohen Zeiger steht dort ein delete und es ist ganz klar, dass der Destruktor stehen bleiben muss.

    Optimizer schrieb:

    Das ganze Idiom ist halt ne Krücke, wenn es jemand grundsätzlich ablehnt, verstehe ich das sogar. Ansonsten gehört es IMHO schon zum guten Ton, ein automatisches Objekt zu verwenden, außer es bereitet wirklich Extra-Aufwand.

    Automatische Objekte treten hier gar nicht auf... in jedem Fall ist die Automatik aber eine Illusion - und Code, der lügt, ist in jedem Fall abzulehnen. Soll der Smartpointer einem doch die Aufgabe abnehmen, über die Zerstörung zum richtigen Zeitpunkt nachdenken zu müssen, geht das in diesem Fall ja offensichtlich völlig fehl.

    // (1)
    class Impl;
    class MyClass
    {
    public:
    ...
        MyClass(const MyClass&);
        MyClass& operator=(const MyClass&);
        ~MyClass();
    private:
        scoped_ptr<Impl> pimpl;
    };
    //
    class Impl
    {
    ....
    };
    MyClass::MyClass(const MyClass& other)
    : pimpl(new Impl(*other.pimpl))
    {
    }
    MyClass& MyClass::operator=(const MyClass& rhs);
    {
        *pimpl = *rhs.pimpl;
        return *this;
    }
    MyClass::~MyClass()
    {
    }
    
    //////////////////////////////////////////////////
    // (2)
    class Impl*;
    class MyClass
    {
    public:
    ...
        MyClass(const MyClass&);
        MyClass& operator=(const MyClass&);
        ~MyClass();
    private:
        Impl* pimpl;
    };
    //
    class Impl
    {
    ....
    };
    MyClass::MyClass(const MyClass& other)
    : pimpl(new Impl(*other.pimpl))
    {
    }
    MyClass& MyClass::operator=(const MyClass& rhs);
    {
        *pimpl = *rhs.pimpl;
        return *this;
    }
    MyClass::~MyClass()
    {
        delete pimpl;
    }
    
    //////////////////////////////////////////////////
    // (3) noch etwas interessanter und gleich auf const-Korrektheit geachtet
    ///
    class MyClass
    {
    public:
    ...
        MyClass(const MyClass&);
        MyClass& operator=(const MyClass&);
        ~MyClass();
    private:
        void* pimpl;
    };
    //////
    namespace
    {
    class Impl
    {
    ....
    };
    static_assert<sizeof(Impl)<=sizeof(void*)>;
    Impl* impl(void* pimpl, MyClass*) { &return reinterpret_cast<Impl&>(pimpl); }
    const Impl* impl(void* pimpl, const MyClass*) { &return reinterpret_cast<const Impl&>(pimpl); }
    #define PIMPL impl( pimpl, this )
    }
    MyClass::MyClass(const MyClass& other)
    {
        new (PIMPL) Impl( *impl( other.pimpl, &other ) );
    }
    MyClass& MyClass::operator=(const MyClass& rhs);
    {
        *PIMPL = *impl( rhs.pimpl, &rhs );
        return *this;
    }
    MyClass::~MyClass()
    {
        PIMPL->~Impl();
    }
    

    Ich sehe wirklich nicht, wie Variante 1 einfacher als Variante 2 ist.

    Nexus Vorschlag schaue ich mir noch in Ruhe an.



  • @camper

    Ich verstehs immer noch ned ! 😃

    wenn delete auf einen Zeiger angewendet wird, dessen Pointeetyp unvollständig definiert ist?

    unvollstaendig definiert ? Der Code der den destruktor des auto_ptr baut, wird doch an der stelle generiert, wo die vollstaendige definition der klasse verfuegbar ist, bzw sein muss ???
    Das heisst doch, der destruktor code vom autoptr wird durch die cpp der Proxy Klasse generiert. Dort muss der Dtor der IMPL Klasse eben doch vollstaendig definiert sein, also auch der richtige DTor ???
    Waer an der Impl Klasse irgendwas unvollstaendig ... wuerde er den autoptr doch gar ned bauen koennen ???

    Hasst du mal nen beispiel fuer ?

    Ciao ...


  • Mod

    RHBaum schrieb:

    @camper

    Ich verstehs immer noch ned ! 😃

    wenn delete auf einen Zeiger angewendet wird, dessen Pointeetyp unvollständig definiert ist?

    unvollstaendig definiert ? Der Code der den destruktor des auto_ptr baut, wird doch an der stelle generiert, wo die vollstaendige definition der klasse verfuegbar ist, bzw sein muss ???
    Das heisst doch, der destruktor code vom autoptr wird durch die cpp der Proxy Klasse generiert. Dort muss der Dtor der IMPL Klasse eben doch vollstaendig definiert sein, also auch der richtige DTor ???
    Waer an der Impl Klasse irgendwas unvollstaendig ... wuerde er den autoptr doch gar ned bauen koennen ???

    Hasst du mal nen beispiel fuer ?

    Ciao ...

    Der Destruktor von auto_ptr wird in jeder Übersetzungseinheit instantiiert, die diesen aufruft (weil 1. Der Destruktor von auto_ptr dort definiert ist - Definition steht im Header <memory> - und 2. der Aufruf des Destruktors impliziert, dass der Destruktor irgendwo im Programm existiert). Ist der Destruktor der Schnittstellenklasse also der compilergenerierte, ist das jede ÜE, die diesen compilergenerierten Destruktor aufruft. Somit wird der Destruktor (von auto_ptr) ggf. in verschiedenen ÜEs instantiiert - und ggf. mit verschiedenem Inhalt. Schlimmer noch: wenn man nicht aufpasst, wird der Destruktor von auto_ptr in der ÜE, die die Impl-Implemention enthält, gar nicht instantiiert. Jedenfalls haben wir es dann mit Instantiierungen verschiedenen Inhalts zu tun und welche Version verwendet wird, ist mehr oder weniger Zufalls (abhängig von Linkeroptionen und inline-Substitution).
    delete kann auf einen Zeiger mit unvollständigem Typ angewendet werden, ruft dann aber nat. keinen Destruktor auf und benutzt den globalen delete-Operator.
    Hat der entsprechende Impl-Typ aber einen nicht-trivialen Destruktor, oder wurde der delete-Operator dafür überladen, ist das undefiniert.



  • Ahhh, ok, langsam komm ich glaub ich dahinter.

    Das Problem wuerde also massiv auftreten, wenn Du die auto_ptr mit ihrer Kopiersemantik nutzt, also die autoptr durchreichst, und dein autoptr irgendwo den scope komplett verliert ohne Zuweisung(also der destruktor aufgerufen wird) in einer ÜE, wo die vollstaendige definition des Pointers ned zur verfuegung steht ... also nur nen Forward vorhanden ist (und der compiler dann nen automatischen DTor geniert) richtig ???

    also bei einer Pimpl wuerde es kaum auftreten, da in der Proxyklasse die definition sowieso brauchst, man will ja da funktionen da aufrufen ???

    Das ist also eher nen Problem der Quelle Senke Thematik, wenn man die Erzeugung und das Zerstoeren in komplett anderen ÜE macht ?

    Die Frage für mich war, ob es sinn macht, den stl auto_ptr, den ich extensiv fuer die Pimpls verwendet habe, durch scoped ptr zu ersetzen, da wir vielleicht demnaechst endlich nen compiler mit tr1 impl bekommen (vc10).
    Denk mal ist eher Formsache, bei den standard pimpl isses wurscht, bei weitergabe (nicht const std::auto_ptr) sollt man vielleicht generell umschwenken. wuerd aber bedeiten, das andere mit älteren compilern die module nimmer bauen koennten ....oder nachtraeglich boost installieren muessten. Verwend den autoptr aber fast wirklich nur const als pimpl.

    Ciao ...


  • Mod

    RHBaum schrieb:

    Ahhh, ok, langsam komm ich glaub ich dahinter.

    Minimalbeispiel:

    //header
    #include <memory>
    struct Impl;
    struct Foo
    {
        void foo();
        std::auto_ptr<Impl> p;
    };
    // ÜE1
    #include "header"
    int main
    {
        Foo x;
    // hier wird Foo::~Foo aufgerufen.
    // Da das der vom Compiler deklarierte Destruktor ist,
    // wird er HIER definiert. Da dieser Destruktor selbst implizt
    // den Destruktor von p aufruft, muss hier ebenso der Destruktor
    // von auto_ptr<Impl> instantiiert werden, Impl ist an dieserr Stelle
    // unvollständig, also erfolgt kein Aufruf von ~ Impl und es wird der
    // globale delete-Operator benutzt
    // weil der vom Compiler generiert Destruktor inline ist, erfolgt
    // hier möglicherweise gar kein richtiger Funktionsaufruf
    }
    
    // ÜE2
    #include "header"
    struct Impl
    {
        void operator delete(void*) {}
        ~Impl() {}
    };
    void Foo::foo()
    {
    }
    // nichts in dieser ÜE bedingt die Instantiierung des Destruktors von Foo
    // oder std::auto_ptr<Impl>
    // selbst wenn das anders wäre, bleibt unbestimmt, welche Version am Ende
    // benutzt wird
    


  • camper schrieb:

    Es ist jetzt klar, dass Destruktor (und Copyctor/Copy-Zuweisung, falls benötigt) selbst implementiert werden müssen trotz der Verwendung von Smartpointern. Das ist in meinen Augen eine extrem negative Bilanz und nicht etwa ein "geringer Vorteil", weil es unnötig Aufmerksamkeit erfordert. An einem schlechten Tag hat man all diese Zusammenhänge auch schon mal wieder vergessen, schaut sich den Destruktor an, der scheinbar leer ist, und ersetzt ihn wieder durch den vom Compiler generierten (möglich jedenfalls bei auto_ptr oder smart_ptr ohne zu Compilerfehlern zu führen). Bei einem rohen Zeiger steht dort ein delete und es ist ganz klar, dass der Destruktor stehen bleiben muss.

    Die Gefahr sehe ich bei einem Destruktor mit delete genauso. Wenn jemand meint, meinen Code verbessern zu müssen und wenigstens etwas Skill hat, wird er den Destruktor zu entfernen versuchen und den Member durch einen smart pointer ersetzen. Dann kriegt er die Warnung (oder Fehler mit deinem scoped_ptr) und die Sache ist geklärt.

    Einen leeren Destruktor sieht man btw. ziemlich oft, zum Beispiel wenn man ihn virtual haben will (selbst abstrakte Destruktoren müssen definiert werden), sollte also niemanden übermäßig aufregen.

    Optimizer schrieb:

    Das ganze Idiom ist halt ne Krücke, wenn es jemand grundsätzlich ablehnt, verstehe ich das sogar. Ansonsten gehört es IMHO schon zum guten Ton, ein automatisches Objekt zu verwenden, außer es bereitet wirklich Extra-Aufwand.

    Automatische Objekte treten hier gar nicht auf... in jedem Fall ist die Automatik aber eine Illusion - und Code, der lügt, ist in jedem Fall abzulehnen. Soll der Smartpointer einem doch die Aufgabe abnehmen, über die Zerstörung zum richtigen Zeitpunkt nachdenken zu müssen, geht das in diesem Fall ja offensichtlich völlig fehl.

    Es geht doch gar nichts fehl. Der auto_ptr funktioniert genauso wie er soll, so wie er es immer gemacht hat, ich muss immer noch nicht über den Zeitpunkt der Zerstörung nachdenken und gelogen wird da auch nichts. Ziemlich harte Worte für perfekt funktionierenden Code. Deine Aussagen treffen nur zu, falls das delete im smart pointer nicht klappt, was man ja ohne großen Aufwand verhindern kann.

    Wenn du Panik hast, dass jemand den Destruktor entfernen könnte, kannst du ihn ja kommentieren oder den scoped_ptr verwenden. Schlechter als nen rohen Zeiger zu verwenden ist der Code mit dem smart_ptr sicherlich nicht. Ich hab ja schon eingeräumt, dass der Gewinn auch nicht so riesig ist.



  • Btw. der unique_ptr wird ja einige Schwachstellen vom auto_ptr ausmerzen, vielleicht wird da auch das delete-Problem angegangen.



  • Optimizer schrieb:

    Die Gefahr sehe ich bei einem Destruktor mit delete genauso. Wenn jemand meint, meinen Code verbessern zu müssen und wenigstens etwas Skill hat, wird er den Destruktor zu entfernen versuchen und den Member durch einen smart pointer ersetzen. Dann kriegt er die Warnung (oder Fehler mit deinem scoped_ptr) und die Sache ist geklärt.

    Oder er benutzt gerade einen Compiler/eine Warnstufe, mit der die Warnung nicht erkennbar ist, lässt die Warnung unter vielen unwichtigen untergehen oder ist einen Moment unachtsam und kompiliert danach nicht mehr. Selbst Menschen mit "Skill" passiert sowas dauernd wieder. Du solltest den Vorteil von Kompilierfehlern gegenüber eventuellen Warnungen nicht unterschätzen.

    Optimizer schrieb:

    Einen leeren Destruktor sieht man btw. ziemlich oft, zum Beispiel wenn man ihn virtual haben will (selbst abstrakte Destruktoren müssen definiert werden), sollte also niemanden übermäßig aufregen.

    Genau das wird man sich als Leser auch als erstes denken. Dann merkt man, dass nichts polymorph ist und überlegt sich vielleicht, dass der Destruktor mal benötigt wurde, aber inzwischen durch Änderungen in der Klasse redundant geworden ist. Es gibt sehr wenige Fälle, in denen eine leere Destruktorimplementierung nötig ist - es ist nicht einmal unwahrscheinlich, dass selbst ein guter Programmierer solche Situationen gar nicht kennt, da diese nie zuvor Probleme bereitet haben. Natürlich kann man sein Anliegen durch einen Kommentar verdeutlichen, aber dann hast du wieder etwas, das bei delete nicht nötig ist.

    Optimizer schrieb:

    Schlechter als nen rohen Zeiger zu verwenden ist der Code mit dem smart_ptr sicherlich nicht.

    Smart-Pointer haben zweifellos in sehr vielen Fällen Vorteile gegenüber rohen Zeigern. Dennoch würde ich nicht grundsätzlich davon ausgehen, dass sie nie schlechter sein können (unabhängig von dem Beispiel hier), besonders wenn keine klar überwiegenden Argumente existieren.



  • Nexus schrieb:

    Optimizer schrieb:

    Die Gefahr sehe ich bei einem Destruktor mit delete genauso. Wenn jemand meint, meinen Code verbessern zu müssen und wenigstens etwas Skill hat, wird er den Destruktor zu entfernen versuchen und den Member durch einen smart pointer ersetzen. Dann kriegt er die Warnung (oder Fehler mit deinem scoped_ptr) und die Sache ist geklärt.

    Oder er benutzt gerade einen Compiler/eine Warnstufe, mit der die Warnung nicht erkennbar ist, lässt die Warnung unter vielen unwichtigen untergehen oder ist einen Moment unachtsam und kompiliert danach nicht mehr. Selbst Menschen mit "Skill" passiert sowas dauernd wieder. Du solltest den Vorteil von Kompilierfehlern gegenüber eventuellen Warnungen nicht unterschätzen.

    Jo, ist einsichtig. Dann lass ich mich mal so weit breitschlagen, dass wir nur den scoped_ptr nehmen, der dieses Problem nicht hat (also eigentlich würd ich es ja für ne Schwäche der Sprache halten ;)).

    Natürlich kann man sein Anliegen durch einen Kommentar verdeutlichen, aber dann hast du wieder etwas, das bei delete nicht nötig ist.

    Doch, der Kommentar muss beim delete dann auch hin. Sonst bin ich nämlich der erste, der das delete entfernt und einen smart pointer einsetzt. Und der letzte bin ich nicht...

    Optimizer schrieb:

    Schlechter als nen rohen Zeiger zu verwenden ist der Code mit dem smart_ptr sicherlich nicht.

    Smart-Pointer haben zweifellos in sehr vielen Fällen Vorteile gegenüber rohen Zeigern. Dennoch würde ich nicht grundsätzlich davon ausgehen, dass sie nie schlechter sein können (unabhängig von dem Beispiel hier), besonders wenn keine klar überwiegenden Argumente existieren.

    In diesem Fall find ich den Code mit smart pointer nicht schlechter. Daher würde ich an dem Prinzip festhalten, smart pointer als Member zu benutzen, als default sozusagen.


Anmelden zum Antworten