Deleter für Ptr Klasse



  • Hi, ich habe folgende "MyPtr" Klasse (Minimal-Beispiel).
    Mit dieser kann ich z.B. folgendermaßen ein Objekt anlegen:

    MyPtr<int> pTest(new int(42));
    

    Allerdings funktioniert so der selbe Aufruf mit "const" nicht.
    Denn die Deleter Funktion erwartet einen Typ von "void*".

    MyPtr<const int> pTest(new int(42));
    

    Wer kann mir sagen wie ich den Deleter abändern muss, dass er sowohl für z.B. "MyPtr<int>" und "MyPtr<const int>" funktioniert?

    typedef void (*MyDeleter)(void*);
    
    template <typename T>
    void delete_deleter(void* ptr)
    {
    	printf(_T("deleting...\n"));
    	delete static_cast<T*>(ptr);
            printf(_T("deleted.\n"));
    }
    
    template <typename T>
    class MyPtr
    {
    public:
    	template <typename U>
    	MyPtr(U* ptr, MyDeleter deleter = &delete_deleter<T>) :
    		deleter(deleter),
    		ptr(ptr)
    	{
    	}
    
    	~MyPtr()
    	{
    		deleter(ptr);
    	}
    
    	T* get() const
    	{
    		return ptr;
    	}
    
    private:
    	MyDeleter deleter;
    	T* ptr;
    };
    

  • Mod

    Da du doch offensichtlich Templates kennst, wieso dann noch void*? Das ist irgendwie schizophren.



  • Natürlich könnte man das const einfach wegcasten, aber damit löst du nur ein selbstgemachtes Problem, das du vor allem deshalb hast,
    weil du die schöne Typ-Information T in deinem Deleter einfach so achtlos wegwirfst.

    Das T Nicht-Wegwerfen kann man mit deinem Ansatz z.B. so machen:

    template <typename T>
    void delete_deleter(T* ptr)
    {
        printf(_T("deleting...\n"));
        delete ptr;
        printf(_T("deleted.\n"));
    }
    
    template <typename T>
    class MyPtr
    {
    public:
        typedef void (*MyDeleter)(T*);
    
        template <typename U>
        MyPtr(U* ptr, MyDeleter deleter = &delete_deleter<T>) :
            deleter(deleter),
            ptr(ptr)
        {
        }
    
        ...
    
    private:
        MyDeleter deleter;
        T* ptr;
    };
    

    Allerdings: Warum muss der Deleter eigentlich ein Funktionspointer mit genau dieser Signatur sein? Das ist erstens eine sehr spezifische
    Anforderung an den Deleter, und zweitens lassen sich Funktionspointer meist nicht sonderlich gut vom Compiler inlinen, besonders wenn
    sie eigentlich lediglich zu einem simplen delete "zerfallen" sollten. Ich würde mich da gar nicht so sehr festlegen, sondern einfach sagen
    "Ein Deleter für MyPrt<T> ist ein beliebiges Funktions-Objekt d , welches bei Aufruf mit einem Pointer p vom Typ T in Form von d(p); das
    Objekt löscht, auf das p zeigt."

    Das würde dann in Code etwa so aussehen:

    template <typename T>
    struct default_deleter
    {
        void operator()(T* p)
        {
            delete p;
        }
    };
    
    template <typename T, typename D = default_deleter<T>>
    class MyPtr
    {
    public:
        template <typename U>
        MyPtr(U* ptr, D deleter = {}) :
            deleter(deleter),
            ptr(ptr)
        {
        }
        ...
    private:
        D deleter;
        ...
    }
    

    So macht es z.B. auch std::unique_ptr , den du ebenfalls verwenden solltest, wenn du keinen guten Grund für den eigenen Smartpointer hast,
    oder das nicht zu Übungszwecken dient. Falls du den Klassen-Templateparameter D unbedingt vermeiden willst, dann verwende lieber eine
    std::function<void(T*)> , die akzeptiert wenigstens auch Lambdas und Funktoren. Oder du trickst mit Polymorphie herum - Hauptsache du
    wirfst das schöööne T nicht einfach so auf den Müll 😉

    Edit: Fehler korrigiert: void operator(T* p) -> void operator()(T* p)



  • MyDeleter schrieb:

    Wer kann mir sagen wie ich den Deleter abändern muss, dass er sowohl für z.B. "MyPtr<int>" und "MyPtr<const int>" funktioniert?

    Ganz so wie man annehmen würde 😕

    typedef void (*MyDeleter)(void const volatile*);
    
    template <typename T>
    void delete_deleter(void const volatile* ptr)
    {
        delete static_cast<T const volatile*>(ptr); // Ja, man darf const volatile* deleten!
    }
    
    template <typename T>
    class MyPtr
    {
    public:
        template <typename U>
        MyPtr(U* ptr, MyDeleter deleter = &delete_deleter<T>) :
            deleter(deleter),
            ptr(ptr)
        {
        }
    
        ~MyPtr()
        {
            deleter(ptr);
        }
    
        T* get() const
        {
            return ptr;
        }
    
    private:
        MyDeleter deleter;
        T* ptr;
    };
    
    void test()
    {
        MyPtr<int> i(new int());
        MyPtr<const int> i2(new int());
    }
    

    ps: Clang ab Version 3.2 optimiert test() zu *nichts*. I ❤ Clang.
    (OKOK, ein ret bleibt natürlich. Zu wirklich gar nichts optimiert Clang doch nur Funktionen die eindeutig immer und mit allen möglichen Argumenten UB sind. Oder war das GCC wo ich das gesehen habe? Kann mich grad nicht erinnern.)



  • Hi, danke schonmal für die vielen Tipps. Der Hintergrund, dass ich im Deleter ein void* verwende ist, dass ich nicht in jeder MyPtr Instanz einen Deleter als Member haben möchte wie es z.B. der unique_ptr macht. Der shared_ptr macht hier einen Trick. Er speichert so wie ich das verstanden habe den Deleter im "ReferenceCounter" Objekt, so dass der Deleter für alle shared_ptr geteilt wird und nicht x-Mal im Speicher verweilt. Evtl. sollte ich dann eine Basis-Klasse für diesen Refcounter schreiben und dann davon abgeleitete template Versionen damit dies auch mit MyPtr'n die einen Basisklassenpointer haben funktioniert. Muss ich mal testen, ob das so funktionieren würde.


  • Mod

    Eine unique_ptr-Instanz braucht überhaupt gar keine eigene Instanz des deleters, wenn dieser zustandslos ist (was er sein muss). Dazu ist der deleter ja auch Teil der Templatedefinition.



  • Verstehe ich nicht. Das gepostete Beispiel von Finnegan entspricht ja der Idee einen Deleter so zu implementieren wie es auch beim unique_ptr gemacht wird:

    template <typename T>
    struct default_deleter
    {
        void operator(T* p)
        {
            delete p;
        }
    };
    
    template <typename T, typename D = default_deleter<T>>
    class MyPtr
    {
    public:
        template <typename U>
        MyPtr(U* ptr, D deleter = {}) :
            deleter(deleter),
            ptr(ptr)
        {
        }
        ...
    private:
        D deleter;
        ...
    }
    

    Hier hat man doch eine Membervariable "D deleter". Dies ist ja die default deleter struktur. Wird dafür intern kein zusätzlicher Speicher benötigt?


  • Mod

    Ich kann nicht für obige Implementierung sprechen, aber sizeof(unique_ptr) ist in gängigen Standardbibliotheken die Größe eines Zeigers, also ja, das kann alles wegoptimiert werden - ist schließlich nur ein leeres struct. Und nein, die legen die Verwaltungsdaten nicht auf den Heap.



  • SeppJ schrieb:

    Eine unique_ptr-Instanz braucht überhaupt gar keine eigene Instanz des deleters, wenn dieser zustandslos ist (was er sein muss). Dazu ist der deleter ja auch Teil der Templatedefinition.

    Deleter für unique_ptr können sehr wohl zustandsbehaftet sein. Auch wenn das so nicht explizit im Standard steht, siehe 20.8.2.4, " unique_ptr observers":

    deleter_type& get_deleter() noexcept;
    const deleter_type& get_deleter() const noexcept;
    Returns:
    A reference to the stored deleter.

    Allerdings ist tatsächlich üblicherweise sizeof(unique_ptr<T>) == sizeof(T*) , zumindest wenn der Default Deleter verwendet wird.
    Wie das genau bewerkstelligt wird, weiss ich nicht. Möglicherweise über eine Spezialisierung via std::is_empty<T> 1. U.A. deswegen
    ist mein Beispiel oben natürlich nicht exakt wie ein std::unique_ptr . Hoffe davon ging niemand ernstaft aus bei den paar Zeilen 😉

    Ein kurzer Test mit MSVC2015:

    #include <iostream>
    #include <memory>
    
    template <typename T>
    struct StatelessDeleter
    {
        void operator()(T* p)
        {
            delete p;
        }
    };
    
    template <typename T>
    struct StatefulDeleter
    {
        void operator()(T* p)
        {
            delete p;
        }
    
        int state;
    };
    
    auto main() -> int
    {
        std::cout << sizeof(int*) << std::endl;
        std::cout << sizeof(std::unique_ptr<int>) << std::endl;
        std::cout << sizeof(std::unique_ptr<int, StatelessDeleter<int>>) << std::endl;
        std::cout << sizeof(std::unique_ptr<int, StatefulDeleter<int>>) << std::endl;
    
        return 0;
    }
    

    Ausgabe:

    8
    8
    8
    16

    1Beispiel für Spezialisierung mit std::is_empty<T> , so dass zustandslose Deleter keinen zusätzlichen Speicher benötigen:

    #include <type_traits>
    
    template <typename T, typename D, bool = std::is_empty<D>::value>
    struct MyPtrDeleterBase
    {
        D deleter;
    
        MyPtrDeleterBase(D deleter)
        : deleter{ deleter }
        {
        }
    
        void do_delete(T* p)
        {
            deleter(p);
        }
    };
    
    template <typename T, typename D>
    struct MyPtrDeleterBase<T, D, true>
    {
        MyPtrDeleterBase(D deleter)
        {
        }
    
        void do_delete(T* p)
        {
            D{}(p);
        }
    };
    
    template <typename T, typename D = default_deleter<T>>
    class MyPtr : private MyPtrDeleterBase<T, D>
    {
        public:
            template <typename U>
            MyPtr(U* ptr, D deleter = {})
            : MyPtrDeleterBase{ deleter }, ptr{ ptr }
            {
            }
    
            ~MyPtr()
            {
                do_delete(ptr);
            }
    
            T* get() const
            {
                return ptr;
            }
    
        private:
            T* ptr;
    };
    

    Diese Variante nutzt die Empty Base Class Optimisation. Da Daten-Member eine Größe > 0 haben müssen,
    kann der Compiler ansonsten den zustandslosen Deleter nicht wegoptimieren, wenn er ein direkter Member ist.


  • Mod

    Die tatsächliche Implementierung in den bekannten Standardbibliotheken werden sicherlich eine Form von empty base optimization benutzen. Ist viel einfacher als Templatemagie. Der GCC benutzt jedenfalls einfach seinen Tupel, welcher beim GCC genau solch eine Optimierung macht.



  • MyDeleter schrieb:

    Hi, danke schonmal für die vielen Tipps. Der Hintergrund, dass ich im Deleter ein void* verwende ist, dass ich nicht in jeder MyPtr Instanz einen Deleter als Member haben möchte wie es z.B. der unique_ptr macht.

    Machst du in deinem Beispiel aber gerade doch 😕
    Und davon abgesehen... was hat das mit dem Typ der Deleter-Funktion zu tun?

    Wenn der Deleter stateless sein soll, dann kannst du einfach ne Klasse verwenden die ne statische "DeleteIt" Funktion hat. Diese Klasse übergibst du deinem Pointer-Template als Template-Parameter, und dein Pointer-Template ruft sie dann einfach über MyDeleter::DeleteIt(p) auf.

    Und davon wiederrum abgesehen haben die Jungs natürlich Recht wenn sie dich darauf hinweisen dass du bei unique_ptr üblicherweise keinen Overhead hast wenn der Deleter "leer" (Stateless) ist. Und daher gar kein guter Grund besteht ne eigene Smartpointerklasse zu basteln. (Ausser natürlich wenn es dir primär darum geht etwas dabei zu lernen.)



  • hustbaer schrieb:

    Machst du in deinem Beispiel aber gerade doch 😕

    Ja, das war der Tatsache geschuldet, dass ich ein Minimal-Beispiel schreiben wollte :). Eigentlich ist der Deleter dann im ReferenceCounter-Objekt untergebracht. Ich hatte auch schon gelesen, dass der unique_ptr im Vergleich zu einem Raw-Pointer keinen Overhead hat. Aber warum hat man sich dann beim shared_ptr dazu entschieden eine andere Deleter Syntax als beim unique_ptr zu verwenden, wenn es gar keinen Vorteil bringt. Im Gegenteil sogar langsamer ist (Deleter lookup), mehr Speicher braucht und der Code auch noch unschöner ist. Ich denke dann werde ich den Gedanken verwerfen, den Deleter im ReferenceCounterObjekt zu teilen und den Deleter über die Empty Base Class Optimisation versuchen zu implementieren.

    @Finnegan:
    Danke für das Code-Beispiel.


  • Mod

    Ein shared Pointer ist ein ganz anderes Konzept als ein unique_ptr oder gar ein roher Pointer. Du kannst nicht Designentscheidungen von einem auf das andere übertragen.



  • MyDeleter schrieb:

    Ja, das war der Tatsache geschuldet, dass ich ein Minimal-Beispiel schreiben wollte :). Eigentlich ist der Deleter dann im ReferenceCounter-Objekt untergebracht.

    Für nen unique_ptr brauchst du kein ReferenceCounter-Objekt 😕

    MyDeleter schrieb:

    Ich hatte auch schon gelesen, dass der unique_ptr im Vergleich zu einem Raw-Pointer keinen Overhead hat. Aber warum hat man sich dann beim shared_ptr dazu entschieden eine andere Deleter Syntax als beim unique_ptr zu verwenden, wenn es gar keinen Vorteil bringt.

    Natürlich bringt es einen Vorteil. z.B. dass bei shared_ptr der Typ vom Deleter nicht den Typ des Smartpointers beeinflusst. Es ist immer shared_ptr<T> , ganz egal was für einen Deleter man verwendet.

    MyDeleter schrieb:

    Im Gegenteil sogar langsamer ist (Deleter lookup),

    Hast du das gemessen? Der einzige Overhead den ich sehen kann (in der Boost Implementierung) ist ein virtual-call. Den könnte man natürlich loswerden, wenn man sich die Typ-Abhängigkeit eintreten möchte, und den damit einhergehenden Template-Bloat.

    MyDeleter schrieb:

    mehr Speicher braucht

    Wieder: Hast du das gemessen? Wüsste nicht wieso es mehr Speicher brauchen sollte. Bei shared_ptr brauchst du den Control-Block (="ReferenceCounter-Objekt") sowieso. Da noch den Deleter mit reinzustopfen sollte, wenn man es richtig macht (Empty-Base und so), keinen Unterschied machen.

    Weiters wäre es etwas seltsam wenn man den Deleter in jedem Zeiger vorhält (kann ja bei Shared-Ownership mehrere geben die auf das selbe Objekt zeigen). Und auch einiges an Overhead (falls der Deleter nicht "leer" ist). Und es ergeben sich noch ganz andere Probleme. z.B. kann shared_ptr das Objekt immer "passend" löschen, selbst wenn es ein Polymorphes Objekt mit nicht-virtuellem Destruktor in der Basisklasse ist, und der letzte shared_ptr der das Objekt löscht ein shared_ptr<Basisklasse> ist. Eben weil der Deleter mit im Control-Block drinnen steckt, und dieser auf den Typ des Objekts spezialisiert ist mit dem der shared_ptr ursprünglich initialisiert wurde. Versuch das mal mit einer "Overhead-freien" Implementierung hinzubekommen die den Deleter direkt in der Zeiger-Instanz speichert.

    MyDeleter schrieb:

    und der Code auch noch unschöner ist.

    Welcher Code soll unschöner als was sein 😕

    MyDeleter schrieb:

    Ich denke dann werde ich den Gedanken verwerfen, den Deleter im ReferenceCounterObjekt zu teilen und den Deleter über die Empty Base Class Optimisation versuchen zu implementieren.

    Wenn du keine Shared-Ownership brauchst, dann nimm einfach unique_ptr . Bzw. implementiere selbst etwas was ganz ohne Control-Block auskommt, wenn du es unbedingt selbst implementieren willst. Und wenn du doch Shared-Ownership brauchst, dann finde ich den Tradeoff von shared_ptr durchaus sinnvoll. Es vermeidet Template-Bloat und der Overhead ist IMO durchaus akzeptabel. Vor allem da das Löschen/Resetten eines shared_ptr sowieso schon zumindest eine CAS Instruktion braucht (und die sind üblicherweise nicht gerade die schnellsten).


Anmelden zum Antworten