Policy-Based Design für Smart-Pointer
-
Hallo zusammen,
Ich habe momentan sowas (aufs Wesentliche vereinfacht):
template <typename T, template <typename> class OwnershipPolicy> class SmartPtr { public: explicit SmartPtr(T* pointer) : myOwnership() , myPtr(pointer) { } SmartPtr(const SmartPtr& origin) : myOwnership(origin.myOwnership) , myPtr(myOwnership.Copy(origin.myPtr)) { } ~SmartPtr() { myOwnership.Destroy(myPtr); } // ... private: OwnershipPolicy<T> myOwnership; T* myPtr; }Dafür gibt es eine Policy für geteilten Besitz:
template <typename T> struct Shared { Shared() : myRefs(new unsigned int(1)) { } T* Copy(T* originPtr) { ++*myRefs; return originPtr; } void Destroy(T* pointer) { if (--*myRefs == 0) { delete pointer; delete myRefs; } } unsigned int* myRefs; };Etwas merkwürdig, ich weiss. Ich frage mich, wie man so eine Policy gut in das
SmartPtr-Template integrieren kann? Es funktioniert zwar im Moment (sofern mir bei der Vereinfachung keine Fehler unterlaufen sind), aber die Semantik ist ziemlich speziell undSharedist nicht wirklich erweiterbar.Das Problem ist vor allem, dass
Sharedden vomSmartPtrgespeicherten Zeiger nur innerhalb der MethodenCopy()undDestroy()kennt. Im Weiteren wird bei jeder Konstruktion einunsigned intdynamisch angefordert, was teilweise unnötig teuer wird. Blöderweise hat man den gesamten Overhead auch für Nullzeiger. Momentan wird sogar ein Nullzeiger geteilt (d.h. der Counter erhöht sich bei Kopien von Nullzeigern), was es z.B. unmöglich macht, eineShared<T>::IsUnique()-Funktion bereitzustellen, die prüft, ob ein Objekt nur von einemSmartPtrreferenziert wird. Ausserdem verlässt sichSharedauf eine exakte Implementierung vonSmartPtr, was bei einem Refactoring leicht zu Problemen führt und die Möglichkeiten einschränkt.Habt ihr Designvorschläge, wie man sowas besser machen könnte? Ich möchte nicht zu viele Anforderungen an die Policies stellen, da es neben
Sharednoch weitere geben soll. Ich hab mich am Smart-Pointer von Loki orientiert, aber dort gibt es das gleiche Problem.P.S.: Ich kenne
boost::shared_ptr, darum geht es hier nicht.
-
schau dir mal die Loki-Bibliothek bzw. das Buch Modern C++ Design von Alexandrescu an - da wird genau sowas entwickelt und erklärt.
-
pumuckl schrieb:
schau dir mal die Loki-Bibliothek bzw. das Buch Modern C++ Design von Alexandrescu an - da wird genau sowas entwickelt und erklärt.
Es ist genau das. Er sagt ja dass er dort abgekupfert hat.
Er wills halt besser machen als Alexandrescu
Momentan wird sogar ein Nullzeiger geteilt (d.h. der Counter erhöht sich bei Kopien von Nullzeigern), was es z.B. unmöglich macht, eine Shared<T>::IsUnique()-Funktion bereitzustellen, die prüft, ob ein Objekt nur von einem SmartPtr referenziert wird.
Erklär mal wieso... peil ich grad nicht.
-
pumuckl schrieb:
Er wills halt besser machen als Alexandrescu

Sicher.

Ich habe generell etwas andere Anforderungen. Zum Beispiel muss der Klassentyp bei der Deklaration des Smart-Pointers nicht vollständig bekannt sein. Und ich habe auch nicht fünf Policies.

brotbernd schrieb:
Erklär mal wieso... peil ich grad nicht.
Ich hatte gedacht, möglicherweise könnte es nützlich sein, in
Sharedsowas bereitzustellen (falls ich die Policy über Vererbung statt Aggregation importiere, könnte sie das Interface vonSmartPtrvergrössern):bool IsUnique() const { return *myRefs == 1; }Beim Nullzeiger müsste ich dann eine Semantik festlegen (wohl Definitionssache, ob ein leerer Smart-Pointer einzigartig ist oder nicht). Jedoch führt der folgende Code dazu, dass sich die beiden Smart-Pointer
aundbden "Nullzeiger teilen".SmartPtr<T, Shared> a; SmartPtr<T, Shared> b = a; // Kopie erhöht Reference-Counter SmartPtr<T, Shared> c;Das heisst, das
Shared-Objekt von beiden verweist auf denselben Speicher, und dort steht eine 2. Beichingegen steht eine 1, also kann ich momentan nicht sinnvoll anhand des Reference-Counters entscheiden, ob ein Objekt von mehreren Smart-Pointers geteilt wird oder nicht. Zumindest nicht, sofernIsUnique()bei Nullzeigern eine definierte Bedeutung haben soll.Die Alternative wäre natürlich, wenn
Sharedden Zeiger kennen würde und ihn auf Null prüfen könnte. Allerdings müsste ich dann den Zeiger allen Policy-Klassen übergeben und dort speichern. Und das gefällt mir nicht, weil ich dann Informationen verdopple und Aufgabenbereiche vermische. Ausserdem werden viele triviale Policies komplizierter.Und das Problem, dass ich lauter unnötige
new-Allokationen habe, wird dadurch auch nicht gelöst.Ich habe einfach den Eindruck, dieses Policy-Based Design schränkt mich zu stark ein. Mit einer partiellen Templatespezialisierung für
SmartPtr<T, Shared>wäre die Implementierung einfach und effizient. Aber ich suche nach einer Möglichkeit, die Besitz-Semantiken generisch zu behandeln, schliesslich sollen auch weitere Strategien leicht integrierbar sein. Deshalb wäre ich um Design-Tipps sehr froh.
-
Nexus schrieb:
Ich habe generell etwas andere Anforderungen. Zum Beispiel muss der Klassentyp bei der Deklaration des Smart-Pointers nicht vollständig bekannt sein.
Das wird ziemlich schwer bis unmöglich, weil der Smartpointer ja wohl delete aufrufen soll - das Template kann deshalb nur dann instantiiert werden wenn der Destruktor (und damit die ganze Klasse) des Pointees vorliegt.
-
pumuckl schrieb:
Das wird ziemlich schwer bis unmöglich, weil der Smartpointer ja wohl delete aufrufen soll - das Template kann deshalb nur dann instantiiert werden wenn der Destruktor (und damit die ganze Klasse) des Pointees vorliegt.
Der verwendete Typ muss bei der Deklaration des Smart-Pointers nicht bekannt sein. Es reicht, wenn er es später ist. Und damit kann man Abhängigkeiten in Headerdateien schön vermeiden (ähnlich wie bei normalen Zeigern, ein
T*benötigt ja auch keine Definition vonT).
-
Nexus schrieb:
also kann ich momentan nicht sinnvoll anhand des Reference-Counters entscheiden, ob ein Objekt von mehreren Smart-Pointers geteilt wird oder nicht.
Du meinst, du kannst das bei Nullzeiger nicht. Bei echten Objekten musst du es ja können !? Ist denn IsUnique für Nullzeiger überhaupt eine sinnvolle Auskunft?
Ich weiß sowieso nicht, ob eine IsUnique Methode für diese SmartPtr Klasse sinnvoll ist, da sie eigentlich nur in Verbindung mit teilenden Policies einen Sinn ergibt oder?
-
brotbernd schrieb:
Du meinst, du kannst das bei Nullzeiger nicht.
Ja. Im nächsten Satz hätte noch ein "Zumindest..." gestanden.

brotbernd schrieb:
Ich weiß sowieso nicht, ob eine IsUnique Methode für diese SmartPtr Klasse sinnvoll ist, da sie eigentlich nur in Verbindung mit teilenden Policies einen Sinn ergibt oder?
Ja, ich hätte sie ja auch nicht in die
SmartPtr-Klasse, sondern inSharedgesteckt (angenommen,SmartPtrwürde dann vonSharederben).brotbernd schrieb:
Ist denn IsUnique für Nullzeiger überhaupt eine sinnvolle Auskunft?
Das habe ich mir auch überlegt. Jedoch gefällt mir der Gedanke irgendwie nicht, dass man beim Aufruf einfach einen undefinierten Wert zurückbekommt. Obwohl es natürlich eine Option wäre.
Aber
IsUnique()ist ja nicht das einzige Problem. Es zeigt nur schön, dass mein momentanes Policy-Based-Design nicht sehr erweiterbar ist.Ein ähnliches Problem hätte ich, wenn ich eine Move-Semantik à la
std::auto_ptrimplementieren möchte. Da muss man sich eines speziellen Tricks bedienen, um von temporären Objekten einen Konstruktor aufzurufen (der "Kopier"konstruktor nimmt dann eine Non-Const-Referenz, da das Originalobjekt verändert wird). Und für dessen Implementierung habe ich in der Policy-Klasse zu wenig Informationen, während ich in derSmartPtr-Klasse einen Spezialfall für eine einzelne Policy einrichten müsste. Ich muss bereits anhand einesbool-Flags mit Metaprogrammierung feststellen, ob ich nun einenSmartPtr(const SmartPtr&)oderSmartPtr(SmartPtr&)-Konstruktor habe. Ich befürchte, bei weiteren Features brauche ich erneut Spezialbehandlungen.Ich habe nämlich schon einmal was Ähnliches gemacht, allerdings mit Vererbung statt Policies. Ich habe dann eine Basisklasse für gemeinsame Smart-Pointer-Aktionen (Dereferenzierung etc.), und spezifische Funktionalität implementiere ich in abgeleiteten Klassen. Grundsätzlich habe ich dann viel mehr Freiheit, und eben einen
SharedPtr<T>stattSmartPtr<T, Shared>. Aber das benötigt etwas mehr Code(-duplizierung) und hat auch seine Probleme. Hm...
-
Nexus schrieb:
Die Alternative wäre natürlich, wenn
Sharedden Zeiger kennen würde und ihn auf Null prüfen könnte. Allerdings müsste ich dann den Zeiger allen Policy-Klassen übergeben und dort speichern. Und das gefällt mir nicht, weil ich dann Informationen verdopple und Aufgabenbereiche vermische. Ausserdem werden viele triviale Policies komplizierter.Wieso prüfst du nicht einfach in
CopyoboriginPtrein Nullpointer ist? Und falls dies der Fall ist, erhöhst du den Zähler nicht. Natürlich musst du dies dann auch inDestroyabfangen.Nexus schrieb:
Und das Problem, dass ich lauter unnötige
new-Allokationen habe, wird dadurch auch nicht gelöst.Steht alles in Modern C++ Design, wie man sowas behebt. Stichwort: Small Object Allocator

Allerdings würde ich sagen, dass reines Policy Design auch nicht das Gelbe vom Ei ist. Wie so oft, sollte man seine Möglichkeiten kombinieren und nicht alles mit dem gleichen Werkzeug machen. Man kann schon eine Schraube mit dem Hammer in die Wand bekommen, aber ob es sinnvoll ist?

Grüssli
PS: Ja, ich weiss, der Thread ist nun schon ein paar Tage alt. Wollte aber noch darauf antworten und hatte damals keine Zeit.

-
Nexus schrieb:
Der verwendete Typ muss bei der Deklaration des Smart-Pointers nicht bekannt sein. Es reicht, wenn er es später ist. Und damit kann man Abhängigkeiten in Headerdateien schön vermeiden (ähnlich wie bei normalen Zeigern, ein
T*benötigt ja auch keine Definition vonT).Im Gegensatz zu normalen Zeigern brauchst du aber die Definition des Smartpointers und damit der Klasse, sobald du einen Smartpoiter als Member einer anderen Klasse hast.
-
Dravere schrieb:
Wieso prüfst du nicht einfach in
CopyoboriginPtrein Nullpointer ist? Und falls dies der Fall ist, erhöhst du den Zähler nicht. Natürlich musst du dies dann auch inDestroyabfangen.Eigentlich eine gute Idee, aber das Ganze ist ein leider recht komplex. Meiner Ansicht nach viel zu komplex. Die Policies werden momentan unabhängig von den Objektzeigern kopiert (Kopierkonstruktoraufruf vs. expliziter Funktionsaufruf), sodass der Reference-Counter-Zeiger nur kopiert wird. Also hat man innerhalb einer Policy wie
Sharedkeine Wertsemantik, sondern verlässt sich wie schon erwähnt streng auf den Programmpfad inSmartPtr. Und als ob dies nicht schon genug wäre, hat eine zusätzliche Indirektion von mir (siehe unten) zur Folge, dass ich nicht einmal garantieren kann, dassCopy()undDestroy()immer aufgerufen werden.Diese Restriktionen verunmöglichen eine vernünftige und effiziente Implementierung von komplizierteren Ownership-Policies.
Dravere schrieb:
Steht alles in Modern C++ Design, wie man sowas behebt. Stichwort: Small Object Allocator

Das ist mir bewusst, aber im Prinzip wird das Problem am falschen Ort gelöst. Statt die Anzahl Allokationen zu verringern, werden diese einfach schneller gemacht. Unnötig (im Sinne von, mit anderem Design geht es ohne) bleiben einige jedoch weiterhin.
Dravere schrieb:
Allerdings würde ich sagen, dass reines Policy Design auch nicht das Gelbe vom Ei ist. Wie so oft, sollte man seine Möglichkeiten kombinieren und nicht alles mit dem gleichen Werkzeug machen. Man kann schon eine Schraube mit dem Hammer in die Wand bekommen, aber ob es sinnvoll ist?

Du hast Recht. Eigentlich habe ich Policy-Based Design bisher eher sparsam verwendet. Nach dem Lesen von Alexandrescus
SmartPtr-Kapitel habe ich das Gefühl gehabt, es handle sich um einen Fall, in dem sich dieses Design wirklich bewähren würde und auch gut erweiterbar wäre. Allerdings scheine ich das ein wenig zu euphorisch betrachtet zu haben.Es ist eigentlich schade, denn ich habe zuerst ein paar einfache Policies implementiert (z.B. eine, die das referenzierte Objekt kopiert), und das ging sehr gut. Sobald ich aber mit
Sharedeine statusbehaftete Strategie eingeführt habe, wurde alles massiv komplizierter.Dravere schrieb:
PS: Ja, ich weiss, der Thread ist nun schon ein paar Tage alt. Wollte aber noch darauf antworten und hatte damals keine Zeit.

Kein Problem, im Gegenteil. Ich dachte nicht, dass noch jemand antworten würde...

pumuckl schrieb:
Im Gegensatz zu normalen Zeigern brauchst du aber die Definition des Smartpointers und damit der Klasse, sobald du einen Smartpoiter als Member einer anderen Klasse hast.
Normalerweise schon, aber ich habe einen kleinen Trick herausgefunden, mit dem man die Abhängigkeiten sehr stark reduzieren kann (ich war übrigens recht stolz darauf :)). Der verwaltete Typ muss nur beim Aufruf des Konstruktors mit Parametern bekannt sein – bei Defaultkonstruktor, Kopierkonstruktor, Zuweisungsoperator und Destruktor ist die Sichtbarkeit der Definition jedoch nicht nötig. Das ist sehr praktisch, weil man sich die Grossen Drei einer Klasse mit
SharedPtrals Member so durch den Compiler generieren lassen kann (bei der Deklaration vonSharedPtr<T, MyPolicy>mussTnur deklariert sein). Die Abhängigkeit im Parameter-Konstruktor ist hingegen fast nie ein Problem, da man meistens ohnehin gleichnewverwendet und den Typen somit kennen muss.Die Implementierung ist gar nicht so kompliziert: Ich verwende Funktionszeiger, die ich im einen Konstruktor initialisiere. In den anderen Methoden werden die Funktionen durch die Zeiger aufgerufen, was keine Definition von
Terfordert. Den kleinen Laufzeitoverhead durch die dynamische Bindung über Funktionszeiger nehme ich in Kauf. Im Prinzip funktioniert die Indirektion folgendermassen (in Wirklichkeit sieht es noch ein wenig komplexer aus, um Spezialfälle zu unterstützen). Die beiden Dummy-FunktionenDoNothing()undReturnNull()sind für den Defaultkonstruktor konzipiert, der keine Abhängigkeit vonTbesitzt.namespace detail { template <typename T, template <typename> class OwnershipPolicy> T* CustomCopy(T* pointer, OwnershipPolicy<T>& ownership) { // Erzwinge vollständige Definition sizeof(T); return ownership.Copy(pointer); } template <typename T, template <typename> class OwnershipPolicy> T* CustomDestroy(T* pointer, OwnershipPolicy<T>& ownership) { sizeof(T); return ownership.Destroy(pointer); } template <typename T, template <typename> class OwnershipPolicy> void DoNothing(T*, OwnershipPolicy<T>&) { } template <typename T, template <typename> class OwnershipPolicy> T* ReturnNull(T*, OwnershipPolicy<T>&) { return NULL; } } template <typename T, template <typename> class OwnershipPolicy> class SmartPtr { private: OwnershipPolicy<T> myOwnership; T* myPtr; T* (*myCopyFn)(T*, OwnershipPolicy<T>&); void (*myDestroyFn)(T*, OwnershipPolicy<T>&); public: SmartPtr() : myOwnership() , myPtr(NULL) , myCopyFn(&detail::ReturnNull<T, OwnershipPolicy>) , myDestroyFn(&detail::DoNothing<T, OwnershipPolicy>) { } explicit SmartPtr(T* pointer) : myOwnership() , myPtr(pointer) , myCopyFn(&detail::CustomCopy<T, OwnershipPolicy>) , myDestroyFn(&detail::CustomDestroy<T, OwnershipPolicy>) { // lediglich hier muss T vollständig vorliegen } SmartPtr(const SmartPtr& origin) : myOwnership(origin.myOwnership) , myPtr(origin.myCopyFn(origin.myPtr, myOwnership)) , myCopyFn(origin.myCopyFn) , myDestroyFn(origin.myDestroyFn) { } ~SmartPtr() { myDestroyFn(myPtr, myOwnership); } SmartPtr& operator= (const SmartPtr& origin) { SmartPtr tmp(origin); Swap(tmp); return *this; } };
-
Nexus schrieb:
Eigentlich eine gute Idee, aber das Ganze ist ein leider recht komplex. Meiner Ansicht nach viel zu komplex. Die Policies werden momentan unabhängig von den Objektzeigern kopiert (Kopierkonstruktoraufruf vs. expliziter Funktionsaufruf), sodass der Reference-Counter-Zeiger nur kopiert wird. Also hat man innerhalb einer Policy wie
Sharedkeine Wertsemantik, sondern verlässt sich wie schon erwähnt streng auf den Programmpfad inSmartPtr.Ich sehe darin kein Problem. Es geht schliesslich nur um die Aufrufe der Funktionen
T* Copy(T*)undvoid Destroy(T*). Eine Wertsemantik braucht man deswegen nicht.Nexus schrieb:
Und als ob dies nicht schon genug wäre, hat eine zusätzliche Indirektion von mir (siehe unten) zur Folge, dass ich nicht einmal garantieren kann, dass
Copy()undDestroy()immer aufgerufen werden.Damit deine SmartPtr Implementation richtig funktioniert, musst du einen garantierten Aufruf von
Copy()undDestroy()haben. Sonst geht deine Klasse so oder so nicht. Wenn ich mir allerdings deinen Code ansehe, kann ich nicht erkennen, dass du da irgendwo keine Garantie dafür abgeben könntest.Nexus schrieb:
Das ist mir bewusst, aber im Prinzip wird das Problem am falschen Ort gelöst. Statt die Anzahl Allokationen zu verringern, werden diese einfach schneller gemacht. Unnötig (im Sinne von, mit anderem Design geht es ohne) bleiben einige jedoch weiterhin.
Nein, das Problem wird absolut am richtigen Ort gelöst. Das Problem sind hier nicht die vielen kleinen Reservationen. Wenn man etwas auf dem Heap legen muss, dann muss man es auch dort ablegen. Egal wie klein oder wie oft. Das Problem ist wirklich nur, dass bei kleinen Speicherreservierungen
newoft sehr langsam ist. Deshalb führt man den Small Object Allocator ein, um diesem Geschwindigkeitsnachteil entgegen zu kommen.Scheu dich nicht davor, etwas auf dem Heap abzulegen, sei es noch so klein.
Nexus schrieb:
Du hast Recht. Eigentlich habe ich Policy-Based Design bisher eher sparsam verwendet. Nach dem Lesen von Alexandrescus
SmartPtr-Kapitel habe ich das Gefühl gehabt, es handle sich um einen Fall, in dem sich dieses Design wirklich bewähren würde und auch gut erweiterbar wäre. Allerdings scheine ich das ein wenig zu euphorisch betrachtet zu haben.Ja, sein Buch lässt einem sehr euphorisch werden. Ich war nach dem Lesen des Buches auch ganz aus dem Häuschen. Wenn man die Problematik aber wieder in der Realität anschaut, ist es oft nicht so einfach zu lösen, wie es sich Alexandrescu wohl vorgestellt hat und vor allem überhaupt nicht wartbar. Wenn wirklich alle Entwickler, welche am C++ Code mitentwickeln, Profis wären, dann wäre es wohl kein Problem und der Code würde wirklich wartbarer werden, wie es sich Alexandrescu vorstellt. Aber leider ist das eine utopische Vorstellung und man kann nicht davon ausgehen, dass nur Profis am Code arbeiten werden.
Aber sein Buch zeigt sehr schöne Alternative Wege auf, welche man im Kopf behalten sollte. Für gewisse Dinge kann man so sehr schöne und einfache Lösungen entwerfen.
Nexus schrieb:
Normalerweise schon, aber ich habe einen kleinen Trick herausgefunden, mit dem man die Abhängigkeiten sehr stark reduzieren kann (ich war übrigens recht stolz darauf :)).
Ich will dir ja nicht die Laune verderben, aber die Smart-Pointer von Boost können dies schon lange

Grüssli
-
Dravere schrieb:
Damit deine SmartPtr Implementation richtig funktioniert, musst du einen garantierten Aufruf von
Copy()undDestroy()haben. Sonst geht deine Klasse so oder so nicht. Wenn ich mir allerdings deinen Code ansehe, kann ich nicht erkennen, dass du da irgendwo keine Garantie dafür abgeben könntest.Doch; wenn ich nämlich den
SmartPtr-Standardkonstruktor aufrufe, dann zeigen die Funktionszeiger auf Dummy-Funktionen.Dravere schrieb:
Nein, das Problem wird absolut am richtigen Ort gelöst. Das Problem sind hier nicht die vielen kleinen Reservationen. Wenn man etwas auf dem Heap legen muss, dann muss man es auch dort ablegen.
Nein, man schafft sich Probleme, die man mit einer anderen Technik eindämmt, aber nicht behebt. Genau darum geht es mir: Ich kann mit einer besseren Implementierung Allokationen vermeiden. Nämlich dort, wo nur ein Nullzeiger gespeichert wird. Dafür braucht man keinen Reference-Counter.
Dravere schrieb:
Scheu dich nicht davor, etwas auf dem Heap abzulegen, sei es noch so klein.
Warum das? Natürlich überlege ich es mir gut, bevor ich etwas auf den Heap lege, und vermeide es wo möglich und sinnvoll. Und verwende tendenziell eher
std::vector<char>alsstd::list<char>. Was ist daran schlecht?Gerade wenn man keinen Small-Object-Allocator nutzen kann, ist sowas entscheidend. Aber auch mit diesem schadet es nicht, sich genauere Überlegungen anzustellen.
Dravere schrieb:
Ich will dir ja nicht die Laune verderben, aber die Smart-Pointer von Boost können dies schon lange

Das ist mir bewusst, aber ich habe da nie länger darüber nachgedacht. Und nahezu alles hat es wohl in irgendeinem C++-Code schon gegeben.