Missing clone_ptr / value_ptr



  • Nein du brauchst auch keine clone Funktion. Alle Membervariablen werden durch den Aufruf ihres Copy Constructors kopiert und der value_ptr implementiert diesen eben so, dass nicht die Adresse kopiert wird sondern tatsächlich die Daten. Wobei wir ja schon diskutiert haben das dieser Kopiervorgang für polymorphe Objekte nicht so einfach ist. Ohne Polymorpie ist das allerdings trivial.



  • Dafür brauch ich aber keinen value_ptr weil der Copy C'tor macht das doch schon!?



  • Aber falsch. Wenn du Raw Pointer benutzt kopiert er nur die Adressen und wenn du unique_ptr benutzt gibts gar keinen Default Copy Constructor weil unique_ptr Move only ist.



  • nach dem ansehen von 'inheritance is the base class of evil' bin ich der Meinung das ich bis dahin immer viel zu viel polymorphe Objekte gebaut habe und deswegen alles so kompliziert wurde. Sean Parent legt da einfach seine value Objekte von unterschiedlichen Typen in den Container, sehr fesch die Methode!

    Ansonst, mit nur einem Pointer auf die Base Klasse wird ohne weitere Info auch ein value_ptr Probleme haben irgendetwas brachbar zu kopieren, von selber geht da also nix.



  • sebi707 schrieb:

    Aber falsch. Wenn du Raw Pointer benutzt kopiert er nur die Adressen und wenn du unique_ptr benutzt gibts gar keinen Default Copy Constructor weil unique_ptr Move only ist.

    Die Frage ist, wieso willst du "Pointer kopieren"? Wenn dein Typ Wertsemantik hat und du besitzende Zeiger drauf hast, hast du sehr wahrscheinlich was falsch gemacht...



  • dot schrieb:

    Naja großartig, dann hast du deinen Copy C'tor halt clone() genannt, implementieren musst du es trozdem. Ich ging davon aus, dass es selbstverständlich ist, dass man, wenn man schon meint, so ein clone() Teil haben zu müssen, den Copy C'tor private oder protected implementiert (oder defaulted) und dann

    auto clone() const { return std::make_unique<Blub>(*this); }
    

    macht, weil wieso würde man irgendwas anderes machen... 😉

    Das wurde doch schon alles diskutiert: es sollte möglich sein dass der value_ptr den normalen Copy-Ctor verwendet.

    dot schrieb:

    Dafür brauch ich aber keinen value_ptr weil der Copy C'tor macht das doch schon!?

    Wenn du mir zeigst wie du nen Baum oder ne verkettete Liste ohne Zeiger aufbaust...



  • hustbaer schrieb:

    dot schrieb:

    Dafür brauch ich aber keinen value_ptr weil der Copy C'tor macht das doch schon!?

    Wenn du mir zeigst wie du nen Baum oder ne verkettete Liste ohne Zeiger aufbaust...

    Die Frage ist, wo ich dabei die Ziele irgendwelcher Pointer kopieren muss...



  • @dot
    Meine Geduld wird gerade sehr strapaziert.

    Wie kopierste deinen Baum denn?
    Wie meinste kopiert sich std::set ?
    Indem es Nodes kopiert, die Zeiger auf weitere Nodes haben, die auch wieder kopiert werden müssen usw.



  • Wenn ich einen Typen mit Wertsemantik hab, steck ich die Objekte direkt in den Container, weil wieso würde ich eine sinnlose Indirektion haben wollen!? Wenn ich Typen mit Referenzsemantik hab, mach ich mir lieber eine Funktion, die mir per make_unique(**it) eine Deep Copy anfertigt (denn das ist die einzige Stelle, an der ich das brauche), bevor ich solch fragwürdige Konstrukte wie einen value_ptr in freie Wildbahn entlasse...



  • @dot
    Vergiss vielleicht einfach mal den Begriff Zeiger.
    Statt dessen denk an boost::optional .
    Wenn wir polymorphie mal aussen vor lassen, dann ist das Ding worüber wir reden ein boost::optional welches rekursive Datenstrukturen erlaubt.



  • Nun, dann kann man ja einfach boost::optional verwenden; wobei ich persönlich das niemals tun würde, weil ich nie wieder ruhig schlafen könnte...



  • Ich habe mal aus Spaß an der Freude gerade nochmal eine clone_ptr -Implementierung runtergetippt. vielleicht nützt das ja irgendeinem hier.

    Das Ding verwendet zwei Templateparameter, wobei der zweite für das Funktionsobjekt da ist, was das Klonen durchführt. Ein default_clone Objekt kommt sowohl mit trivialen Typen als auch polymorphen Klassen klar, die eine entsprechende clone() -Methode anbieten. Hier ist drauf zu achten, dass der Rückgabetyp der clone() -Methode zum statischen Typ der Klasse passen sollte. C++ erlaubt hier ja in einer abgeleiteten Klasse statt Base* auch Derived* zurückzugeben. Wenn also ein clone_ptr<Derived> kopiert werden soll, braucht man für den Klon ja wieder ein Derived* und kein Base* .

    Andere Implementierungsdetails:

    • Die Typen fallback_on_copy und prefer_cloning sind dazu dar, bei der Überladungsauflösung die Überladung mit Argumenttyp prefer_cloning zu bevorzugen (siehe Tag Dispatching).
    • Bei den Funktionstemplates default_clone_impl "findet SFINAE statt" wegen ->decltype statt. Deswegen benutze ich da auch identity . Ich will ja, dass die Rückgabe von clone implizit in ein T* wandelbar ist (wenn es das noch nicht ist). Wenn die Ausdrücke in ->decltype(...) "nicht klappen", wird das Template einfach ignoriert.
    • Ein clone_ptr<Derived> zu einem clone_ptr<Base> zu konvertieren, wobei der clone_ptr<Base> das Objekt dann immer noch korrekt löschen kann, geht ja nur, wenn der Destruktor von Base virtuell ist. Das habe ich per SFINAE als Bedingung für den Konvertierungskonstruktor gemacht, damit man sich nicht in den Fuß schießen kann, wenn man vergisst, den Destruktor virtuell zu machen.
    • Als Datenelement verwende ich ein std::tuple<> , statt Zeiger und Cloner separat zu halten. Das erlaubt die "empty base class optimization" von tuple. Diesen Trick habe ich von typischen unique_ptr -Implementierungen abgeguckt. Er führt dazu, dass "leere Clone-Typen" nicht zur Größe des clone_ptr s beitragen, so dass der clone_ptr genauso groß wie ein roher Zeiger ist.
    #include <iostream>
    #include <type_traits>
    #include <tuple>
    #include <utility>
    #include <algorithm>
    
    struct fallback_on_copy {};
    struct prefer_cloning : fallback_on_copy {}; // more specialized!
    
    template<class T>
    auto identity(T x) -> T { return x; }
    
    template<class T>
    auto default_clone_impl(T const* ptr, fallback_on_copy) -> decltype(new T(*ptr))
    { return new T(*ptr); }
    
    template<class T> // this is preferred if both overloads are viable
    auto default_clone_impl(T const* ptr, prefer_cloning) -> decltype(identity<T*>(ptr->clone()))
    { return ptr->clone(); }
    
    struct default_clone
    {
    	template<class T>
    	auto operator()(T const* ptr) const -> decltype(default_clone_impl(ptr, prefer_cloning{}))
    	{ return default_clone_impl(ptr, prefer_cloning{}); }
    };
    
    template<class T, class Clone = default_clone>
    struct clone_ptr
    {
    	std::tuple<T*,Clone> members_;
    
    	T*      & ptr_()       { return std::get<0>(members_); }
    	T* const& ptr_() const { return std::get<0>(members_); }
    	Clone      & clone_()       { return std::get<1>(members_); }
    	Clone const& clone_() const { return std::get<1>(members_); }
    
    	template<class, class> friend class clone_ptr;
    
    public:
    	explicit clone_ptr(T* ptr = nullptr, Clone c = Clone{})
    	: members_(ptr, std::move(c)) {}
    
    	~clone_ptr() { delete ptr_(); }
    
    	clone_ptr(clone_ptr const& lvalue)
    	: members_(lvalue.clone_()(lvalue.ptr_()), lvalue.clone_()) {}
    
    	clone_ptr(clone_ptr && rvalue)
    	: members_(rvalue.ptr_(), std::move(rvalue.clone_()))
    	{ rvalue.ptr_() = nullptr; }
    
    	clone_ptr& operator=(clone_ptr temp)
    	{
    		std::swap(this->ptr_(), temp.ptr_());
    		std::swap(this->clone_(), temp.clone_());
    	}
    
    	template<class U,
    		class = typename std::enable_if<
    			!std::is_same<T, U>::value &&
    			std::is_convertible<U*, T*>::value &&
    			std::has_virtual_destructor<T>::value
    		>::type>
    	clone_ptr(clone_ptr<U> temp)
    	: members_(temp.ptr_(), std::move(temp.clone_()))
    	{ temp.ptr_() = nullptr; }
    
    	T      & operator*()       { return *ptr_(); }
    	T const& operator*() const { return *ptr_(); }
    	T      * operator->()       { return ptr_(); }
    	T const* operator->() const { return ptr_(); }
    };
    
    template<class T, class...Args>
    auto make_clonable(Args&&...args) -> clone_ptr<T>
    { return clone_ptr<T> { new T(std::forward<Args>(args)...) }; }
    
    struct Base
    {
    	virtual ~Base() {}
    	virtual Base* clone() const = 0;
    };
    
    struct Derived : Base
    {
    	Derived() { std::cout << "Derived created\n"; }
    	Derived(Derived const&) { std::cout << "Derived copied\n"; }
    	~Derived() { std::cout << "Derived destroyed\n"; }
    	virtual Derived* clone() const override { return new Derived(*this); }
    };
    
    int main() {
    	auto c1 = make_clonable<int>(23);
    	auto c2 = c1; // cloning
    	clone_ptr<Derived> d = make_clonable<Derived>();
    	clone_ptr<Base> p = d; // cloning + implicit conversion 
    	clone_ptr<Base> q = p; // cloning (via virtual dispatch)
    	return 0;
    }
    

    🙂



  • Vielleicht ein kleiner Tipp: Dein Clone Typ wird in der Regel leer sein, statt diesem std::tuple Konstrukt (wieso auch immer das std::tuple ) besser private von Clone ableiten und sich auf Empty-Base-Class-Optimization verlassen, sonst wird dein clone_ptr zwangsweise immer größer sein als ein einfacher Pointer und damit unnötig ineffizient beim Herumreichen... 😉



  • dot schrieb:

    Nun, dann kann man ja einfach boost::optional verwenden;

    Nein, kannst du nicht weil das originale boost::optional eben keine rekursiven Datenstrukturen unterstützt.

    dot schrieb:

    wobei ich persönlich das niemals tun würde, weil ich nie wieder ruhig schlafen könnte...

    *facepalm*
    OK.



  • hustbaer schrieb:

    dot schrieb:

    wobei ich persönlich das niemals tun würde, weil ich nie wieder ruhig schlafen könnte...

    *facepalm*
    OK.

    Sorry, ich war gerade etwas verwirrt und hab boost::optional mit boost::variant verwechselt (die Verwendung von sowas wie dem letzterem ist imo als Verbrechen anzusehen). Ich versteh worauf du hinauswillst, nur imo zahlt es sich nicht aus, extra einen value_ptr zu schreiben, nur weil ich in der einen Zeile in der einen Funktion, die eine Deep-Copy machen soll eine Kopie vom Ziel eines Pointers anfertigen will. Ich sag ja nicht, dass ich mir absolut keine Situation vorstellen kann, wo ein value_ptr vielleicht Sinn machen würde, nur ist eine solche Situation wirklich extremst selten...



  • dot schrieb:

    Vielleicht ein kleiner Tipp: Dein Clone Typ wird in der Regel leer sein, statt diesem std::tuple Konstrukt (wieso auch immer das std::tuple ) besser private von Clone ableiten und sich auf Empty-Base-Class-Optimization verlassen, sonst wird dein clone_ptr zwangsweise immer größer sein als ein einfacher Pointer und damit unnötig ineffizient beim Herumreichen... 😉

    Du hast meinen Beitrag offensichtlich nur überflogen. Genau das Thema habe ich sowohl berücksichtigt als auch angesprochen. Ich verlasse mich darauf, dass std::tuple mir die EBO abnimmt. Ist zwar nicht garantiert, aber bei mir klappt's. Ich habe absichtlich keine Vererbung hier eingesetzt, weil das die Menge der Clone-Typen auf "class types" einschränken würde. Man würde hier vielleicht auch einen Funktionszeiger nutzen wollen. Das ginge dann mit der Vererbung so nicht bzw nur dann, wenn man noch eine extra Klasse baut:

    template<class T>
    struct wrapper
    {
       T value;
    };
    
    template<class T, class Clone = default_clone>
    class clone_ptr : wrapper<Clone>
    { ...
    


  • dot schrieb:

    Sorry, ich war gerade etwas verwirrt und hab boost::optional mit boost::variant verwechselt.

    OK, dann verstehe ich dein Kommentar 🙂



  • krümelkacker schrieb:

    ...

    Verstehe. So einen Wrapper wollte ich gerade vorschlagen...

    Dass deine std::tuple Implementierung leere Elemente kollabiert, überrascht mich ehrlich gesagt, denn das würde bedeuten, dass zwei Elemente eines std::tuple die selbe Adresse haben können, was ich als Defekt im Standard ansehen würde, wenn der das tatsächlich erlaubt...

    Edit: Grad getestet: in Visual C++ 2015 wird ein solches Tuple auf x64, wie ich erwartet hätte, 16 Byte groß...



  • Man könnte boost::compressed_pair verwenden
    http://www.boost.org/doc/libs/1_60_0/libs/utility/doc/html/compressed_pair.html

    z.T. tuple hab' ich das gefunden:
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2533.html

    tuple permits an additional space-saving optimization, as exploited in the boost compressed_pair library.

    Bei std::pair ist das AFAIK nicht erlaubt.
    Vermute mal dass das der Grund ist warum krümelkacker tuple statt pair verwendet hat.



  • Gut zu wissen, konnte im Standard auf den ersten Blick weder eine explizite Erlaubnis, noch ein explizites Verbot einer solchen Optimierung finden. Auf jeden Fall eine interessante Frage, hoffentlich weiß jemand mehr...


Anmelden zum Antworten