Smartpointer mit eigenem Allokator?



  • Hallo,

    ich habe mehrere Klassen die Resourcen über std::unique_ptr managen. Das funktioniert soweit auch sehr gut.

    Jetzt muss ich das Programm aber dahingehend abändern dass ein eigener Allokator verwendet wird. Damit ich jetzt nicht an allen Stellen wo ich einen std::make_unique Aufruf habe diesen abändern muss, will ich lieber eine skalierbare Lösung so dass ich einfach und schnell den Allokator ändern kann...

    Nur wie würde man sowas am besten machen?

    • std::make_unique für die jeweiligen Klassen überladen mittels template-Spezialisierung?
      Nachteil: Ich habe mal gelesen dass man keine Elemente in std hinzufügen soll/darf. Außerdem was wäre wenn man trotzdem noch das "normale" std::make_unique braucht?
    • Ein make_unique im eigenen namespace überladen?
      Nachteil: Dann muss ich aber einweder in dem namespace oder in jeder einzelnen Funktion ein using namespace std; einfügen.
    • Den operator new für den Typ überladen?
      Nachteil: Wieder ungünstig wenn sowohl Typen mit dem eigenem Allokator als auch mit dem "default" Allokator angelegt werden sollen können.

    Außerdem müsste ich dann Overloads für mehrere verschiedene Klassen schreiben, was auch wieder redundant wäre...

    Wie würde man das Problem am geschicktesten lösen?



  • Kuck dir doch mal den zweiten Template-Parameter von unique_ptr an.



  • rewrew schrieb:

    Kuck dir doch mal den zweiten Template-Parameter von unique_ptr an.

    Der ist mir bekannt, hat aber leider überhaupt nichts mit der Frage tun.



  • @happystudent
    In std überladen ist böse. Manche Sachen sind in std erlaubt (IIRC swap spezialisieren und so) und machen auch Sinn, aber generell würde ich aus std die Finger raus lassen.

    Am ehesten noch ne eigene Funktion in deinem Namespace (dort wo deine anderen Klassen bzw. Utility-Funktionen leben). Die würde ich dann auch nicht unbedingt make_unique nennen, schonmal deswegen nicht weil ich es verwirrend finde wenn Dinge gleich heissen wie etwas aus der Standard-Library. Und irgend ein passenderer Name fällt dir da sicher ein. Zur Not, wenn dein Allokator Foo heisst, dann MakeUniqueInFoo oder sowas.



  • Der Einwurf von rewrew ist in so weit berechtigt, da ja auch am Ende der Lebeszeit des Objekts ein dealloc gerufen werden muss. Und hier wäre es IMHO aus den schon von Dir genannten Gründen ratsam einen eigene Struktur zu wählen, die aber von der Klasse des allokierten Objekts unabhängig ist. Mit anderen Worten - ein eigenes Deleter-Template ist die saubere Lösung!

    Damit kommst Du um eine Änderung des Anwender-Codes nicht herum. Ob Du das dann make_my_unique<> nennst oder make_unique<> in einem eigenen namespace ist Deinem Gusto überlassen.

    Gruß
    Werner



  • Werner Salomon schrieb:

    Der Einwurf von rewrew ist in so weit berechtigt, da ja auch am Ende der Lebeszeit des Objekts ein dealloc gerufen werden muss. Und hier wäre es IMHO aus den schon von Dir genannten Gründen ratsam einen eigene Struktur zu wählen, die aber von der Klasse des allokierten Objekts unabhängig ist. Mit anderen Worten - ein eigenes Deleter-Template ist die saubere Lösung!r

    Schon, aber ich war davon ausgegangen dass das selbstverständlich ist 😉

    Werner Salomon schrieb:

    Damit kommst Du um eine Änderung des Anwender-Codes nicht herum. Ob Du das dann make_my_unique<> nennst oder make_unique<> in einem eigenen namespace ist Deinem Gusto überlassen.

    hustbaer schrieb:

    Am ehesten noch ne eigene Funktion in deinem Namespace (dort wo deine anderen Klassen bzw. Utility-Funktionen leben). Die würde ich dann auch nicht unbedingt make_unique nennen, schonmal deswegen nicht weil ich es verwirrend finde wenn Dinge gleich heissen wie etwas aus der Standard-Library. Und irgend ein passenderer Name fällt dir da sicher ein. Zur Not, wenn dein Allokator Foo heisst, dann MakeUniqueInFoo oder sowas.

    Genau aber das stört mich, weil eigentlich ist das doch eine symetrische Operation - ein custom deleter macht ja in vielen Fällen nur Sinn wenn auch das Erstellen "custom" war. Also ein custom deleter der free benutzt macht nur Sinn wenn das ursprüngliche Objekt auch mit malloc erstellt wurde.

    Während man den deleter schön bequem per template Parameter konfigurieren kann ist das für das Erstellen irgendwie nicht so ohne weiteres vorgesehen... Zumindest ist eine freie Funktion ala my_make_unique auch ungünstig, weil die ja dann wieder nicht per Parameter konfigurierbar ist.

    Was mir vorschwebt ist sowas in der Art:

    #include <iostream>
    #include <memory>
    
    template <typename SmartPointer, typename Allocator>
    struct A
    {
    	using value_type = typename std::remove_reference<decltype(*SmartPointer())>::type;
    	SmartPointer ptr;
    	A(value_type val) : ptr(Allocator::alloc(val)) {}
    };
    
    template <typename T>
    struct my_alloc
    {
    	template <typename ...Types>
    	static T *alloc(Types&&... args) { return static_cast<T*>(::new (malloc(sizeof(T))) T(std::forward<Types>(args)...)); }
    };
    
    struct my_delete
    {
    	template <typename T>
    	void operator()(T *t) { t->~T(); free(t); }
    };
    
    int main()
    {
    	using A_type = A<std::unique_ptr<int, my_delete>, my_alloc<int>>;
    
    	A_type a(5);
    
    	std::cout << *a.ptr << "\n";
    }
    

    Nur, ist das eine "gute" Lösung des Problems oder gehts noch einfacher/besser?



  • Freie Funktion.



  • rewrew schrieb:

    Freie Funktion.

    Und wie?

    Wenn ich eine Funktion als template Parameter übergeben will müsste ich ja wieder eine feste Anzahl an Parametern vorgeben, oder den allocator als Membervariable speichern.



  • Ich bin verwirrt. Was ist eigentlich dein Ziel? Kannst du das anhand eines einfachen Beispiels zeigen? Also welches konkrete Problem willst du lösen/was willst du erreichen?



  • Laut deinem Code reicht eine freie Funktion und ein Deleter. Die freie Funktion nimmt ein Template-Argument und weiterhin eine variable Anzahl an Elementen entgegen. Dann gibst halt nen Pointer zurück, ob smart oder roh, Latte.



  • rewrew schrieb:

    Laut deinem Code reicht eine freie Funktion und ein Deleter. Die freie Funktion nimmt ein Template-Argument und weiterhin eine variable Anzahl an Elementen entgegen. Dann gibst halt nen Pointer zurück, ob smart oder roh, Latte.

    Ja schon, aber wie kann ich so eine Funktion dann als template Argument an mein struct A übergeben?

    hustbaer schrieb:

    Ich bin verwirrt. Was ist eigentlich dein Ziel? Kannst du das anhand eines einfachen Beispiels zeigen? Also welches konkrete Problem willst du lösen/was willst du erreichen?

    Also ausgehend von dem geposteten Code: A ist eine Klasse die entsprechende Daten über Smartpointer hält und anlegt. Da anlegen und löschen ja quasi "symmetrisch" sein müssen (new/delete bzw malloc/free etc.) sollen sowohl Allokation als auch löschen von außen per template Parameter erledigt werden.

    Für das Löschen ist das kein Problem, man übergibt einfach einen smartpointer mit custom deleter (im Code my_delete ).

    Nur soll das selbe jetzt auch für die Applikation möglich sein, sodass die konkrete allokation in der eigentlichen Klasse A über einen generischen Allocator::alloc(...) Aufruf möglich ist (im Code Zeile 9) - so können dann verschiedene konkrete Instanzen der Klasse über entsprechende template Argumente erstellt werden, die jeweils unterschiedliche, aber passende allocate/delete Paare haben



  • Sorry, aber ich verstehe immer noch nicht was du erreichen willst.
    Ich sehe dein Beispielprogramm, ich sehe was du machst, aber ich verstehe nicht warum. Und ich finde es furchtbar. Die Antwort auf deine Frage ob es eine gute Lösung ist ist also vermutlich "nein". Nur vermutlich, weil wenn ich die Frage/die Intention nicht verstehe, dann kann ich natürlich nicht sicher sein 😉



  • Ich verstehe das Problem immer noch nicht ganz...

    Aber...:

    struct A {};
    
    struct AFactory
    {
    	struct Deleter
    	{
    		void operator() (A* a)
    		{
    			if (a)
    			{
    				a->~A();
    				free(a);
    			}
    		}
    	};
    
    	typedef std::unique_ptr<A, Deleter> Pointer;
    
    	static Pointer Create()
    	{
    		void* storage = malloc(sizeof(A)); // ACHTUNG: Ja, das kann Leaken wenn A::A gleich was wirft. Hier gehört ein Guard hin. Da es in dem Beispiel aber um 'was anderes geht...
    		return Pointer(new(storage) A());
    	}
    };
    
    int main()
    {
    	AFactory::Pointer a = AFactory::Create();
    }
    

    So hättest du alles beinander in einer Klasse.

    Mich persönlich würde ja eher stören dass ich immer unterschiedliche Smart-Pointer Typen für die unterschiedlichen Deleter brauche. Aber das scheint dich ja nicht zu stören. Und wenn, dann liesse es sich normalerweise auch lösen indem man den Deleter-Typ z.B. auf nen Funktionszeiger festlegt. Welche Funktion Deleter spielt könnte (bzw. müsste) man dann in der Factory bei der Erstellung des Smart-Pointer angeben.

    Nachteil ist natürlich dass die Pointer dadurch grösser werden.

    Und geht natürlich nicht wenn man Deleter mit ganz unterschiedlichen Parametern hat. Also z.B. ein Deleter für nen Pool bräuchte dann ja noch nen Zeiger auf den Pool zusätzlich zum freizugebenden Objekt.



  • Ich versteh das ganze Larrifarri einfach nicht.

    Naja egal, dann poste ich auch mal ne Lösung:

    #include <iostream>
    #include <cstdlib>
    #include <memory>
    
    template<typename T, typename... Args>
    T* old_new(Args&&... arguments){
        return new(std::malloc(sizeof(T))) T{std::forward<Args>(arguments)...};
    }
    
    template<typename T>
    struct old_deleter{
        void operator()(T* p){
            std::free(p);
        }
    };
    
    template<typename T>
    using old_and_smart = std::unique_ptr<T, old_deleter<T>>;
    
    struct fx{
        int x, y, z;
    };
    
    int main(){
        old_and_smart<fx> p{old_new<fx>(1, 2, 3)};
        std::cout << (p->x + p->y + p->z) << '\n';
    }
    


  • Gibt aber immernoch ein Speicherleck, wenn der Konstruktor der Klasse etwas wirft.



  • Techel schrieb:

    Gibt aber immernoch ein Speicherleck, wenn der Konstruktor der Klasse etwas wirft.

    Dann halt im make_unique ein try {} catch(..) { } um das new nach dem allocate des Allocators.



  • hustbaer schrieb:

    Sorry, aber ich verstehe immer noch nicht was du erreichen willst.
    Ich sehe dein Beispielprogramm, ich sehe was du machst, aber ich verstehe nicht warum. Und ich finde es furchtbar. Die Antwort auf deine Frage ob es eine gute Lösung ist ist also vermutlich "nein". Nur vermutlich, weil wenn ich die Frage/die Intention nicht verstehe, dann kann ich natürlich nicht sicher sein 😉

    Ok, ich werde es mal ein detailierteres Beispiel zeigen. Ausgehend von folgendem Fall:

    struct A
    {
        std::unique_ptr<int> ptr;
        A(int value) : ptr(std::make_unique(value)) {} // In der Klasse gibt es viele make_unique Aufrufe
    }
    

    Das ist die Ausgangslage. Was an diesem gekürztem Beispiel eventuell fehlt, ist dass A mehrere Resourcen hält (nicht nur einen ptr ) und mehrere Memberfunktionen hat die auch per make_unique Resourcen erstellen.

    Problem dabei: Wenn ein custom Allocator verwendet werden soll, muss an jeder Stelle in der Klasse an der ein std::make_unique Aufruf steht dieser durch einen Aufruf des eigenen Allocators ersetzt werden. Dies kann natürlich in einer (z.B.) freien Funktion "gebündelt" werden wie folgt:

    template <typename T, typename ...Types>
    static T *make_my_smartpointer(Types&&... args) { return static_cast<T*>(::new (malloc(sizeof(T))) T(std::forward<Types>(args)...)); }
    
    template<typename T>
    struct deleter
    {
    	void operator()(T* p){ std::free(p); }
    };
    
    template <typename SmartPointer>
    struct A
    {
    	SmartPointer ptr;
    	A(int value) : ptr(make_my_smartpointer<int>(value)) {}
    };
    

    Vorteil: ich muss die direkte Implementierung in A nicht mehr anfassen. Wenn sich der Allokator ändert muss ich nur den deleter und make_my_smartpointer ändern. Also zwei Änderungen anstatt n Änderungen, wobei n die Anzahl der Allokationen mittels std::make_unique bzw. jetzt eben make_my_smartpointer ist.

    Das ist jetzt in etwa die selbe Lösung wie von:

    rewrew schrieb:

    Naja egal, dann poste ich auch mal ne Lösung:

    Problem dabei ist aber: Was ist wenn ich zwei (ode mehr) verschiedene Versionen von A brauche - erste Version standard, zweite Version mit malloc/free, dritte Version mit was ganz anderem etc.

    Dann funktioniert das mit der freien Funktion so nicht mehr, weil ich von außen, per template Parameter, nur den SmartPointer (und damit nur den deleter ) konfigurieren kann, nicht aber den Allocator. Dieser wird ja über die freie Funktion und damit für alle gleich aufgerufen.

    Deswegen der vorgeschlagene Code.

    hustbaer schrieb:

    Ich verstehe das Problem immer noch nicht ganz...

    Aber...:

    So hättest du alles beinander in einer Klasse.

    Aber das ist ja quasi das gleiche wie mein Code (nur das du my_delete in my_alloc integriert hast).

    In der Klasse würde das ja dann so aussehen:

    template <typename Factory>
    struct A
    {
        using ptr_type = Factory::Pointer;
        using value_type = typename std::remove_reference<decltype(*ptr_type())>::type;
        ptr_type ptr;
        A(value_type val) : ptr(Factory::Create(val)) {}
    };
    

    was ja auch wieder mehr oder weniger das selbe ist?

    Vom Prinzip her wird auch das selbe erreicht, nämlich dass man sich beim schreiben der Klasse keine Gedanken machen muss welcher Allocator jetzt verwendet wird, da das ja jetzt weg-abstrahiert ist. Nur wieso ist das dann nicht furchtbar?



  • Dude, warum willst du eigentlich malloc/free verwenden?



  • malloc/free scheint sehr offensichtlich nur ein Beispiel zu sein. 😉



  • rewrew schrieb:

    Dude, warum willst du eigentlich malloc/free verwenden?

    Letztendlich das hier:

    Ethon schrieb:

    malloc/free scheint sehr offensichtlich nur ein Beispiel zu sein. 😉

    Vielleicht noch zur Frage "warum": Ich arbeite in mit einer anderen Sprache, welche über ein eigenes Interface die Möglichkeit bietet C++ Funktionalität zu integrieren. Die Sprache hat einen Garbage Collector, welchen ich gerne für meine C++ Objekte mitnutzen möchte. Dafür bietet das Interface eigene malloc / free Varianten an (also nicht die üblichen malloc / free aus C). Diese registrieren die jeweiligen Objekte für den Garbage Collector.

    Das wäre der in etwa der Hintergrund. Für die eigentliche Fragestellung sollte das aber relativ egal sein, da sich diese ja auch auf andere Fälle, in denen man eigene Allocator/Deleter verwenden will, übertragen lässt.



  • Gut dass ich "vermutlich" geschreiben habe 🙂 Ich hatte deine struct A falsch verstanden, ich dachte du willst pro Resource eine eigene Instanz von struct A anlegen. Quasi nochmal nen Wrapper um den Smart-Pointer, damit du bei der Verwendug den Allokator nicht mehr angeben musst. Und das fände ich schrecklich 😉

    Auch mit diesen neuen Informationen bleibe ich aber bei meinem Vorschlag. Zumindest fände ich es wichtig wenn man der Klasse A alles nötige für die Erzeugung + Zerstörung von Objekten über ein einziges Template Parameter mitgibt. Bzw. von mir aus auch über zwei. Aber nicht eins per Template-Paramter und das andere hardcoded.

    In deinem make_my_smartpointer Beispiel ist es ja so, dass wenn du make_my_smartpointer anpasst, du auch alle Instanzierungen von A-Templates (oder A-ähnlichen B, C, D-Templates) anpassen musst*. Damit immer der passende Deleter übergeben wird. Was mMn. nicht gut ist. Wenn X zu Y passen muss, dann sollte das verwendete System mMn. auch sicherstellen dass immer zusammenpassende X und Y verwendet werden.

    *: OK, wenn du wirklich nur einen einzigen Deleter im ganzen Programm verwendest, dann nicht, dann musst du tatsächlich nur make_my_smartpointer und diesen einen Deleter anpassen. Ist dann aber weniger flexibel, da du dann nicht verschiedene Allokatoren für verschiedene Dinge verwenden kannst. Trotzdem finde ich dass die beiden zusammengehören, und daher auch gemeinsam an Templates übergeben werden sollten.


Anmelden zum Antworten