Policy-Based Design für Smart-Pointer



  • 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 Shared sowas bereitzustellen (falls ich die Policy über Vererbung statt Aggregation importiere, könnte sie das Interface von SmartPtr vergrö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 a und b den "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. Bei c hingegen 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, sofern IsUnique() bei Nullzeigern eine definierte Bedeutung haben soll.

    Die Alternative wäre natürlich, wenn Shared den 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 von T ).



  • 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 in Shared gesteckt (angenommen, SmartPtr würde dann von Shared erben).

    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_ptr implementieren 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 der SmartPtr -Klasse einen Spezialfall für eine einzelne Policy einrichten müsste. Ich muss bereits anhand eines bool -Flags mit Metaprogrammierung feststellen, ob ich nun einen SmartPtr(const SmartPtr&) oder SmartPtr(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> statt SmartPtr<T, Shared> . Aber das benötigt etwas mehr Code(-duplizierung) und hat auch seine Probleme. Hm...


  • Administrator

    Nexus schrieb:

    Die Alternative wäre natürlich, wenn Shared den 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 Copy ob originPtr ein Nullpointer ist? Und falls dies der Fall ist, erhöhst du den Zähler nicht. Natürlich musst du dies dann auch in Destroy abfangen.

    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 von T ).

    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 Copy ob originPtr ein Nullpointer ist? Und falls dies der Fall ist, erhöhst du den Zähler nicht. Natürlich musst du dies dann auch in Destroy abfangen.

    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 Shared keine Wertsemantik, sondern verlässt sich wie schon erwähnt streng auf den Programmpfad in SmartPtr . 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() und Destroy() 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 Shared eine 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 SharedPtr als Member so durch den Compiler generieren lassen kann (bei der Deklaration von SharedPtr<T, MyPolicy> muss T nur deklariert sein). Die Abhängigkeit im Parameter-Konstruktor ist hingegen fast nie ein Problem, da man meistens ohnehin gleich new verwendet 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 T erfordert. 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-Funktionen DoNothing() und ReturnNull() sind für den Defaultkonstruktor konzipiert, der keine Abhängigkeit von T besitzt.

    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;
    		}
    };
    

  • Administrator

    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 Shared keine Wertsemantik, sondern verlässt sich wie schon erwähnt streng auf den Programmpfad in SmartPtr .

    Ich sehe darin kein Problem. Es geht schliesslich nur um die Aufrufe der Funktionen T* Copy(T*) und void 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() und Destroy() immer aufgerufen werden.

    Damit deine SmartPtr Implementation richtig funktioniert, musst du einen garantierten Aufruf von Copy() und Destroy() 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 new oft 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() und Destroy() 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> als std::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.


Anmelden zum Antworten