Smartpointer mit eigenem Allokator?
-
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!rSchon, 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 odermake_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 mitmalloc
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 generischenAllocator::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 seinOk, 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 einenptr
) und mehrere Memberfunktionen hat die auch permake_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 dendeleter
undmake_my_smartpointer
ändern. Also zwei Änderungen anstatt n Änderungen, wobei n die Anzahl der Allokationen mittelsstd::make_unique
bzw. jetzt ebenmake_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 dendeleter
) 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
inmy_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 üblichenmalloc
/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 vonstruct 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 schrecklichAuch 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 dumake_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.
-
hustbaer schrieb:
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 dumake_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, das stimmt natürlich... dann werde ich wohl dazu übergehen ab jetzt immer einfach einen Factory template Parameter für die jeweiligen Klassen zu definieren.
Weil nochmal will ich mir diesen Stress, in allen Klassen die Allkoation zu refactoren, gerne ersparen. Wundert mich eigentlich dass mir so ein Pattern dann noch nie über den Weg gelaufen ist, weil eigentlich hat man das Problem ja immer wenn man eigene Allokatoren verwenden will...
-
Naja in den wenigsten Programmen kommen eigene Allokatoren zum Einsatz