wahl des richtigen smartpointer



  • Schreib selbst einen.

    #include <iostream>
    #include <string>
    #include <utility>
    
    using namespace std;
    
    template<class ValueType>
    struct Copy_ptr{
    	Copy_ptr(ValueType *nt) : value(nt){
    	}
    	~Copy_ptr(){
    		delete value;
    	}
    	Copy_ptr(const Copy_ptr<ValueType> &other){
    		value = new ValueType(*other); //Die einzige wichtige Zeile, Rest ist blabla
    	}
    	Copy_ptr(Copy_ptr<ValueType> &&other) : value(0){
    		swap(value, other.value);
    	}
    	ValueType &operator *(){
    		return *value;
    	}
    	const ValueType &operator *() const{
    		return *value;
    	}
    	ValueType *&operator ->(){
    		return value;
    	}
    	const ValueType *&operator ->() const{
    		return value;
    	}
    	const ValueType &operator =(const Copy_ptr<ValueType> &other){
    		return *value = *other;
    	}
    	const ValueType &operator =(Copy_ptr<ValueType> &&other){
    		swap(value, other.value);
    		return *value;
    	}
    private:
    	ValueType *value;
    };
    
    int main(){
    	Copy_ptr<string> cps(new string("Hallo"));
    	cout << "size(\"" << *cps << "\") = " << cps->size() << '\n';
    	Copy_ptr<string> cps2 = cps;
    	cout << *cps2 << '\n';
    	*cps2 = "Welt";
    	cout << *cps << *cps2 << '\n';
    }
    

    Funktioniert nicht mit arrays. Setzt voraus, dass ValueType einen Copy-Konstruktor hat.



  • nwp3 schrieb:

    //Die einzige wichtige Zeile, Rest ist blabla

    Viel zu viel Code.

    template <typename T>
    struct dup_policy {
      std::unique_ptr<T> dup(T const& x) { return x.clone(); }
    };
    template <typename T>
    struct copy_policy {
      std::unique_ptr<T> dup(T const& x) { return std::unique_ptr<T>(new T(x)); }
    };
    
    template <class T, class DupPolicy=copy_policy<T> >
    struct copy_ptr : private DupPolicy, private std::unique_ptr<T> {
      using std::unique_ptr<T>::unique_ptr;
      using std::unique_ptr<T>::operator*;
      using std::unique_ptr<T>::operator->;
      copy_ptr(const copy_ptr<T> &o)
        : std::unique_ptr<T>(static_cast<DupPolicy>(*this).dup(*o)) {}
      copy_ptr& operator=(copy_ptr<T>&&)=default;
      copy_ptr& operator=(const copy_ptr<T> &o)
      { return *this = copy_ptr(o); }
    };
    


  • Die Idee mit der dup_policy ist nett, aber intrusive.
    Ich würde hier eher an etwas analog zum "deleter" eines shared_ptr denken. Also ein "copier". Der genau so wie bei shared_ptr nicht in den Typ des Zeigers eingeht.

    Damit wird's natürlich gleich ein schönes Stück komplizierter. Dafür mMn. auch ein schönes Stück einfacher zu verwenden.

    Und den Overhead der 2. Allokation kann man genau wie bei shared_ptr durch eine make_shared -artige Funktion vermeiden.



  • hustbaer schrieb:

    Die Idee mit der dup_policy ist nett, aber intrusive.
    Ich würde hier eher an etwas analog zum "deleter" eines shared_ptr denken. Also ein "copier". Der genau so wie bei shared_ptr nicht in den Typ des Zeigers eingeht.

    Sehe ich nicht so, bei shared_ptr wird erwartungsgemäss sehr selten delete aufgerufen, bei unique_ptr sehr oft. Deshalb ist der deleter bei unique_ptr intrusive. copy_ptr ruft erfahrungsgemäss sehr oft copy auf, deshalb hätte ich das am liebsten ohne Overhead.

    hustbaer schrieb:

    Damit wird's natürlich gleich ein schönes Stück komplizierter. Dafür mMn. auch ein schönes Stück einfacher zu verwenden.

    Mit einem default_dup_policy_trait könnte man umgehen, dass die Policy in der Praxis explizit angegeben werden muss.

    hustbaer schrieb:

    Und den Overhead der 2. Allokation kann man genau wie bei shared_ptr durch eine make_shared -artige Funktion vermeiden.

    Was für eine 2. Allokation?



  • Für mich stellt sich die Frage, wozu so ein copy_ptr gut sein soll, wenn er nicht mit "derived class" Objekten klarkommt.
    D.h. die Default-Variante sollte mMn. damit klarkommen.
    (Oder gibt's da wichtige Anwendungsmöglichkeiten die ich übersehe?)
    Also...

    hustbaer schrieb:

    Damit wird's natürlich gleich ein schönes Stück komplizierter. Dafür mMn. auch ein schönes Stück einfacher zu verwenden.

    Mit einem default_dup_policy_trait könnte man umgehen, dass die Policy in der Praxis explizit angegeben werden muss.

    ...braucht man ne Default-Policy die polymorph "klonen" kann. Nur kann man dummerweise in C++ beliebige Klassen nicht einfach so klonen. Also muss man es irgendwie anders machen.
    Deine Variante, also eine "clone()" Memberfunktion mit genau diesem Namen zu verlangen, ist intrusive. Finde ich also nicht so gut.

    Wenn dann könnte ich mir noch eher folgende "dup_policy" vorstellen, analog zu dem was boost::intrusive_ptr macht:

    template <typename T>
    struct dup_policy {
      std::unique_ptr<T> dup(T const& x)
      {
         // Keine Default-Implementierung von copy_ptr_clone(), wird ggf. über ADL gefunden
         return std::unique_ptr<T>(copy_ptr_clone(x));
      }
    };
    

    Und diese sollte mMn. dann Default sein.
    Alternativ könnte man ein Concept-artiges Hilfs-Klassentemplate verwenden das man für den zu klonenden Typ spezialisieren muss.

    Ansonsten halte ich den Overhead beim Aufruf der Clone-Funktion auch mit Copier für durchaus vertretbar. Das ist ein zusätzlicher virtual Call, damit kann man sicher leben. So nen Zeiger wird man ja wohl nicht für Integers o.ä. verwenden. Die 2. Allokation fände ich da viel schlimmer.

    hustbaer schrieb:

    Und den Overhead der 2. Allokation kann man genau wie bei shared_ptr durch eine make_shared -artige Funktion vermeiden.

    Was für eine 2. Allokation?

    Na der Copier muss ja irgendwo gespeichert werden. Siehe shared_ptr , der macht auch ne 2. Allokation für den Deleter wenn man nicht make_shared verwendet.


  • Mod

    Für mich stellt sich die Frage, wozu so ein copy_ptr gut sein soll, wenn er nicht mit "derived class" Objekten klarkommt.

    Wie meinst du das? Zeiger auf Basisklassen sind Zeiger auf Basisklassen. Du kannst natürlich einen Kopierkonstruktor für copy_ptr<Derived> definieren, der einen copy_ptr<Base> nimmt und auf
    dessen Inhalt dynamic_cast<Derived&>(*other.get()) anwendet.
    Aber du solltest nicht erwarten, dass ein copy_ptr<Base> einen anderen copy_ptr<Base> so kopiert, dass die tatsächlichen most-derived-objects kopiert werden.

    Deine Variante, also eine "clone()" Memberfunktion mit genau diesem Namen zu verlangen, ist intrusive. Finde ich also nicht so gut.

    Du kannst natürlich mit Type-Erasure etwas schönes basteln. Also eine virtuelle clone()-Funktion eines Wrappers. Das ganze stelle ich mir so *ganz grob* vor

    template<typename T>
    struct clone_ptr
    {
        T* ptr;
    
        struct WrapperBase
        {
            virtual clone_ptr<T> clone(T*) = 0;
        }* mWrapper;
    
        template<typename Derived>
        struct Wrapper : WrapperBase
        {
            virtual clone_ptr<T> clone(T* ptr) { return new Derived(dynamic_cast<Derived&>(*ptr)); }
        };
    
        template<typename T2,
                 typename = typename std::enable_if<std::is_base_of<T, T2>::value>::type>
        clone_ptr( T2* c ):
            ptr{c},
            mWrapper{ new Wrapper<T2> ) {}
    };
    

    Bin aber schon sehr müde und sehe mir das morgen Nachmittag genauer an.



  • Ein klassischer "ich weiss was!!!11elf" Beitrag von unserem liebsten "ich weiss was!!!11elf" User. *sigh*

    Arcoth schrieb:

    Für mich stellt sich die Frage, wozu so ein copy_ptr gut sein soll, wenn er nicht mit "derived class" Objekten klarkommt.

    Wie meinst du das? Zeiger auf Basisklassen sind Zeiger auf Basisklassen.
    (*snip-nonsense*)

    Ach watt!

    Wenn ich einen copy_ptr<Base> habe, wo ein Derived drinnen steckt, und ich kopiere mir daraus einen 2. copy_ptr<Base> , dann soll da wieder ein Derived drin stecken.

    Du kannst natürlich mit Type-Erasure etwas schönes basteln.

    Wenn du den Thread aufmerksam gelesen hättest, wäre dir vielleicht aufgefallen, dass das mein 1. Vorschlag war.

    Also eine virtuelle clone()-Funktion eines Wrappers. Das ganze stelle ich mir so *ganz grob* vor
    (*snip*)

    *sigh*
    Ja, ich denke die meisten hier wissen wie man sowas angeht.
    Und eine nichtmal 1/2-fertige Lösung ist wohl kaum interessant.

    Davon abgesehen kann eine Klasse die (ausser einer Memberfunktion) nichts enthält wohl auch nichts "Wrappen".
    => Cloner, Copier, aber sicher nicht Wrapper.
    Und der dynamic_cast ist auch problematisch. Erstens wollen wir für den ja nicht bezahlen, und zweitens wollen wir von T auch nicht verlangen dass es polymorph sein muss.



  • Wozu zerbrecht ihr euch den Kopf? Ich habe oben einen Link zu einer Bibliothek gepostet, die eine fertige Implementierung für einen Smart-Pointer mit Deep-Copy-Semantik anbietet, sogar über polymorphe Objekte ohne clone()-Funktion hinweg.

    Die Verwendung ist äusserst simpel:

    aurora::CopiedPtr<Base> a(new Derived);
    aurora::CopiedPtr<Base> b = a; // erstellt neues Derived-Objekt
    

    Ausserdem kann man weitere Cloner- und Deleter-Strategien angeben.



  • Aurorer schrieb:

    Wozu zerbrecht ihr euch den Kopf? Ich habe oben einen Link zu einer Bibliothek gepostet, die eine fertige Implementierung für einen Smart-Pointer mit Deep-Copy-Semantik anbietet, sogar über polymorphe Objekte ohne clone()-Funktion hinweg.

    Die Lib erfordert sehr wohl eine vituelle clone-Funktion.



  • nwp3 schrieb:

    Die Lib erfordert sehr wohl eine vituelle clone-Funktion.

    Tut sie nicht, du guckst nicht richtig.

    @Aurorer
    Weil es für manche vielleicht auch interessant ist zu wissen wie bestimmte Dinge funktionieren.
    Davon abgesehen ist die Implementierung die du verlinkt hast durchaus verbesserungswürdig.



  • Aurorer schrieb:

    Deep-Copy-Semantik anbietet, sogar über polymorphe Objekte ohne clone()-Funktion hinweg.

    Ich bin mir gar nicht so sicher, ob das gut ist.



  • @volkard
    Was würdest du mit einem kopierenden Zeiger anstellen der das nicht macht? Also der beim Kopieren ein Objekt der Basisklasse erstellt (slicing).
    In so einem Fall würde ich gleich den gewünschten Typ direkt verwenden. Oder ggf. boost::optional<T> , wenn ich lazy initialization bzw. allgemein einen "empty" Zustand brauche.

    Oder meinst du so ein Zeiger grundsätzlich ist gut, aber er sollte nicht "einfach so" funktionieren?



  • hustbaer schrieb:

    nwp3 schrieb:

    Die Lib erfordert sehr wohl eine vituelle clone-Funktion.

    Tut sie nicht, du guckst nicht richtig.

    Ok, ich habe nicht bis zu ende gelesen. Ich mag es trotzdem nicht. Man muss im gesamten Code aufpassen, dass man immer den dynamischen Typ übergibt. Nicht wirklich ein Vorteil gegenüber einer vergessbaren clone-Funktion, wo man nur in den Klassendeklarationen nach einem fehlenden clone suchen muss.
    Man warte auf C++2x, dann gibts virtuelle Copy-Konstruktoren und dann funktioniert das out of the box.

    (Ich hasse es englisch und deutsch zu mischen)


  • Mod

    nwp3 schrieb:

    Man muss im gesamten Code aufpassen, dass man immer den dynamischen Typ übergibt.

    Auch nur dann, wenn man zeitweise mit rohen Zeigern arbeitet, was bei Smartpointern i.d.R. vermeidbar ist. Typischerweise stammen die aus irgendwelchen Factories oder eben einem einfachen make_clone_ptr.



  • Bzgl Kopieren/Clonen hatte ich das mal ungefähr so gelöst:

    #define AUTO_RETURN(...) ->decltype(__VA_ARGS__) {return __VA_ARGS__;}
    
    namespace clone_details {
    
    struct fallback_on_copy {};
    struct prefer_clone : fallback_on_copy {};
    
    template<class T>
    auto doit(T const* ptr, fallback_on_copy)
    AUTO_RETURN( new T(*ptr) )
    
    template<class T>
    auto doit(T const* ptr, prefer_clone)
    AUTO_RETURN( ptr->clone() )
    
    } // clone_details
    
    template<class T>
    struct default_clone_destroy
    {
        auto clone(T const* ptr)
        AUTO_RETURN(clone_details::doit(ptr,prefer_clone()))
    
        void destroy(T* ptr)
        { delete ptr; }
    };
    

    (so runtergetippt und nicht getestet)

    ...so dass man in den meisten Fällen einfach default_clone_destroy zum Klonen und Löschen benutzen kann


  • Mod

    Mir faellt noch eine Variante ein, grundlegend saehe das so aus:

    #include <type_traits>
    #include <utility>
    
    template< typename T >
    class clone_ptr_polymorph
    {
        void* mDerivedPtr;
        T* mBasicPtr;
        std::pair<T*, void*> (* mCloner)(void*);  
    
        template< typename Derived >
        static std::pair<T*, void*> clone(void* ptr)
        {
        	auto obj = new Derived{ *reinterpret_cast<Derived*>(ptr) };
        	return { obj, obj };
        }
    
    public:
        template< typename Derived,
                  typename=typename std::enable_if<std::is_base_of<T, Derived>::value>::type >
        clone_ptr_polymorph( Derived* ptr ) :
            mDerivedPtr{ptr},
            mBasicPtr{ptr},
            mCloner{ &clone<Derived> } {}
    
        clone_ptr_polymorph( clone_ptr_polymorph const& p ) :
            mCloner{p.mCloner}
        {
        	auto pair = mCloner(p.mDerivedPtr);
        	mBasicPtr = pair.first;
        	mDerivedPtr = pair.second;
        }
    
        ~clone_ptr_polymorph()
        {
        	delete mBasicPtr;
        }
    
        clone_ptr_polymorph( clone_ptr_polymorph&& ) = default;
        clone_ptr_polymorph& operator=( clone_ptr_polymorph&& ) = default;
    };
    
    template<typename Base,
             typename Der,
             typename... Args>
    clone_ptr_polymorph<Base> make_clone_ptr( Args&&... args )
    {
    	return { new Der(std::forward<Args>(args)...) };
    }
    
    #include <iostream>
    
    struct Base
    {
    	virtual ~Base() { std::cout << "Base-Destructor\n"; }
    	Base() { std::cout << "Base-Def-Ctor\n"; }
    	Base(Base const&) { std::cout << "Base-Copy-Ctor\n"; }
    	Base(Base&&) { std::cout << "Base-Move-Ctor\n"; }
    };
    
    struct Derived : Base
    {
    	~Derived() { std::cout << "Derived-Destructor\n"; }
    	Derived() { std::cout << "Derived-Def-Ctor\n"; }
    	Derived(Derived const&) { std::cout << "Derived-Copy-Ctor\n"; }
    	Derived(Derived&&) { std::cout << "Derived-Move-Ctor\n"; }
    };
    
    int main()
    {
    	auto const& p = make_clone_ptr<Base, Derived>();
    	auto second = p;
    }
    

    http://ideone.com/MQ2xTv
    (Bin gerade nicht konzentriert, da in der Schule, aber so sollte wenigstens hustbaer nicht einen halben Fetzen darin sehen)

    Edit: Ich glaube, der Ideone Output ist merkwuerdig... habe ich da boese rumgepfuscht? wieso taucht da
    Base-Def-Ctor
    Derived-Copy-Ctor
    auf?



  • @hustbaer: Kann gut sein, was würdest du konkret verbessern?

    @nwp3: Man muss den dynamischen Typen nur bei der Initialisierung angeben. In vielen Fällen kennt man ihn dann, weil man das Objekt gerade konstruiert. Und ansonsten nimmt man eben die clone-Strategie.



  • Ach nein, der (die Ausgabe) ist richtig. Ich habe den copy-ctor ja selbst definiert. :schaem:



  • hustbaer schrieb:

    @volkard
    Was würdest du mit einem kopierenden Zeiger anstellen der das nicht macht? Also der beim Kopieren ein Objekt der Basisklasse erstellt (slicing).
    In so einem Fall würde ich gleich den gewünschten Typ direkt verwenden. Oder ggf. boost::optional<T> , wenn ich lazy initialization bzw. allgemein einen "empty" Zustand brauche.

    Oder meinst du so ein Zeiger grundsätzlich ist gut, aber er sollte nicht "einfach so" funktionieren?

    Ich meinte, daß es evtl gar nicht gut ist, den Benutzer nicht zu zwingen, zu diesem Zwecke clone() selber anzubieten und in die Klassen zu schreiben, hübsch bei den großen 3/5/6.



  • Der richtige smart pointer hat einen Default-Konstruktor, der automatisch eine Instanz des Programmes das man haben möchte erzeugt. Alles andere ist doch nur Augenwischerei.


Anmelden zum Antworten