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; };
-
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-InformationT
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ürMyPrt<T>
ist ein beliebiges Funktions-Objektd
, welches bei Aufruf mit einem Pointerp
vom TypT
in Form vond(p);
das
Objekt löscht, auf dasp
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-TemplateparameterD
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öööneT
nicht einfach so auf den MüllEdit: 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, einret
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.
-
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?
-
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 viastd::is_empty<T>
1. U.A. deswegen
ist mein Beispiel oben natürlich nicht exakt wie einstd::unique_ptr
. Hoffe davon ging niemand ernstaft aus bei den paar ZeilenEin 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
161Beispiel 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.
-
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.
-
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-ObjektMyDeleter 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 immershared_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 letzteshared_ptr
der das Objekt löscht einshared_ptr<Basisklasse>
ist. Eben weil der Deleter mit im Control-Block drinnen steckt, und dieser auf den Typ des Objekts spezialisiert ist mit dem dershared_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 vonshared_ptr
durchaus sinnvoll. Es vermeidet Template-Bloat und der Overhead ist IMO durchaus akzeptabel. Vor allem da das Löschen/Resetten einesshared_ptr
sowieso schon zumindest eine CAS Instruktion braucht (und die sind üblicherweise nicht gerade die schnellsten).