Designfrage: Mehrfachvererbung / Interfaces
-
Aloha,
ich schreibe gerade an einer kleinen Game-Engine und bin dabei auf ein kleines Problem gestoßen, bei dem ich noch nicht genau weiß, wie ich es lösen soll. Dabei geht es mir weniger darum, wie ich es lösen kann (Lösungen hätte ich theoretisch schon), sondern eher darum, welche in diesem Fall die "richtige" Lösung wäre. Vielleicht hat dazu jemand von euch einen kleinen Denkanstoß für mich
Bei dem Problem geht es um folgende Klassen:
class SharedObject { void retain(); void release(); } class Node : public SharedObject { ... } class TouchReceiver { virtual void onTouchBegin(...); } class MyMagicButton : public Node, public TouchReceiver { ... }
SharedObject ist die Basis-Klasse für die meisten Klassen die ich verwende und implementiert einen Referenz-Zähler, mit dem ich das Speicher-Management implementiere.
Node ist die Basisklasse für Objekte im Scene-Graph.
TouchHandler will ich gerade implementieren und soll eine Interface-Klasse werden, die von Objekten im Scene-Graph benutzt werden soll, welche Touch- bzw. Maus-Events empfangen sollen.
MyMagicButton ist ein Beispiel für einen Button, der irgendwo im Scene-Graph liegt, und auf Klicks reagiert.Dazu muss er sowohl von Node, als auch von TouchReceiver erben.Mein Problem ist nun folgendes:
Die Engine, welche Touches, bzw. Maus-Events verwaltet, sucht sich die Node, die von dem Event getroffen wird, und speichert sich dieses ab, um auch alle Folge-Events an dieses weiter zu leiten. Damit die Engine nicht irgendwann einen ungültigen Pointer hält, müsste sie auch den Referenz-Counter der Node inkrementieren. Ansonsten kann es z.B. passieren, daß beim Klick auf den Button das Menü geschlossen und der Button gelöscht wird. Allerdings ist TouchReceiver kein SharedObject, welches den Referenzzähler implementiert.Nun, welche Möglichkeiten habe ich?
Per dynamic_cast das TouchReceiver Objekt auf SharedObject casten?
Erscheint mir nicht allzu sinnvoll.SharedObject als virtuelle Basisklasse sowohl für Node als auch TouchReceiver?
Somit wäre TouchReceiver in jedem Fall immer ein SharedObject und man könnte den Referenzzähler direkt ohne irgendwelche castereien benutzen.
Allerdings fände ich es inkonsequent, wenn SharedObject nur bei diesen beiden Klassen eine virtuelle Basisklasse ist. Konsequenterweise sollte SO dann immer virutell sein, was mir allerdings ein wenig übertrieben scheint. Zudem sehe ich TouchReceiver eher als schlankes Interface und will es nicht zu sehr überladen.Eine TouchNode, die direkt von Node erbt?
Würde das Problem "Mehrfachvererbung" umgehen. Die TouchNode könnte ein Container sein, der die Maus-Events für alle seine Children behandelt, somit bestünde auch die Möglichkeit, jede beliebige Node "touchable" zu machen. Der Scene-Graph würde allerdings ein wenig komplexer werden, und für eine Button-Klasse müsste ich zwei Objekte verwalten (eines für die Events, eines für das Zeichnen). Kommen noch weitere Interfaces (z.B. KeyEventReceiver, etc) dazu, wird es nicht einfacher...TouchReceiver erbt nicht von SharedObject, sondern sorgt dafür, daß er in seinem Destruktor der Engine selbständig mitteilt, daß sie seinen Pointer vergessen möge
Kommt auch ohne Mehrfachvererbung aus, erscheint mir aber auch ein wenig fragwürdig im Design, wenn ansonsten jedes andere Objekt einen Referenzzähler besitzt, der TouchReceiver aber nicht.So, falls noch jemand bis hierher mitgelesen hat: Welche Variante würdet ihr bevorzugen? Pro oder Contra Mehrfachvererbung? Gibt es noch eine Möglichkeit, auf die ich noch nicht gekommen bin? Ich freue mich auf eure Meinung
-
Dein Designfehler ist das intrusive Referenzzählen.
-
Ein Artikel den ich dir sehr zu Herzen legen würde: http://scientificninja.com/blog/write-games-not-engines
Glaub mir, ich weiß warum
-
Hm, man könnte das ganze auch als Framework bezeichnen, man fängt ja auch nicht bei jedem Spiel komplett von null an und schreibt alle Klassen für Scenes, Pointer-Handling, etc neu
Aktuell ist es für mich eine Spielerei, um ein paar Dinge auszuprobieren. Obs tatsächlich mal ein Spiel wird, ist erst einmal Nebensache.@PI
Was würdest du stattdessen vorschlagen? Ich bin da durchaus für Ideen offen. Da das ganze ein privates Projekt ist, hindert mich ja auch niemand daran, alles über den Haufen zu werfen und neu/besser zu implementierenIch hab allerdings anfangs schon diese Variante gegen die Verwenung von Helfern wie shared_ptr abgewogen und fand diese Variante angenehmer. Ich muss allerdings auch zugeben, daß ich bisher nur diese beiden Mechanismen kenne.
-
shared_ptr ist wieder nur Reference Counting. Meiner Erfahrung nach braucht man Reference Counting äußerst selten (praktisch nie). Überleg dir stattdessen, wie die Besitzverhältnisse wirklich aussehen und mach die Objekte dann direkt zu Membern oder lokalen Objekten. Immer ein guter Freund ist std::unique_ptr.
Scope ist nichts wogegen man kämpfen muss, sondern potentiell einer der stärksten Verbündeten die ein Programmierer hat...
-
314159265358979 schrieb:
Dein Designfehler ist das intrusive Referenzzählen.
was ist daran schlecht?
-
So ganz einfach ist es auch nicht, zu sagen daß Objekte nur von ihren Besitzern zerstört werden sollen.
Wenn ich zum Beispiel nun in meinem Spiel ein Dialogfeld anzeige, kann man natürlich sagen, das Dialogfeld gehört dem Application Objekt und soll von diesem gelöscht werden, wenn ich es schließe. Aber wie sieht es mit den Buttons oder Textfeldern auf dem Dialogfeld aus? Eigentlich möchte ich doch lieber, daß die mit dem Dialogfeld direkt mit gelöscht werden, und mir keine Liste mit Objekten merken, bzw. wann die gelöscht werden dürfen.
Oder ich will einen Scene-Wechsel mit Hilfe einer Transition durchführen, dann möchte ich, daß die Transition mein altes Scene-Objekt löschen kann, sobald die Transition beendet ist.Und natürlich gibt es Fälle wie den obigen, daß z.B. ein TouchHandler ein Objekt behalten kann, was sonst bereits gelöscht wäre.
Ist natürlich kein Allheilmittel und hat auch seine eigenen Nachteile, auf die man achten muss. Für die aktuelle Aufgabe erschien es mir aber bisher die geeigneteste Lösung zu sein.
Ist jetzt nicht so, daß ich noch kein Spiel geschrieben hätte, welches diese Art von Speichermanagement benutzt, und bisher hat es eigentlich immer recht zuverlässig funktioniert
-
cdtgfvhbj schrieb:
was ist daran schlecht?
Was ist daran gut? Intrusives refcounting hat nur Nachteile gegenüber shared_ptr. Der gröbste dürfte wohl sein, dass es nicht automatisch funktioniert.
-
Baldur schrieb:
Wenn ich zum Beispiel nun in meinem Spiel ein Dialogfeld anzeige, kann man natürlich sagen, das Dialogfeld gehört dem Application Objekt und soll von diesem gelöscht werden, wenn ich es schließe.
Mit anderen Worten: Du möchtest dass das Dialogfeld ein Member der Application ist...
Baldur schrieb:
Aber wie sieht es mit den Buttons oder Textfeldern auf dem Dialogfeld aus? Eigentlich möchte ich doch lieber, daß die mit dem Dialogfeld direkt mit gelöscht werden, und mir keine Liste mit Objekten merken, bzw. wann die gelöscht werden dürfen.
Mit anderen Worten: Du möchtest dass die Buttons und Textfelder Member des Dialogfeldes sind...
Baldur schrieb:
Oder ich will einen Scene-Wechsel mit Hilfe einer Transition durchführen, dann möchte ich, daß die Transition mein altes Scene-Objekt löschen kann, sobald die Transition beendet ist.
Lässt sich z.B. über std::unique_ptr machen. Was du da beschreibst, klingt mir eher nach Ownership-Transfer und nicht nach shared Ownership...
Baldur schrieb:
Und natürlich gibt es Fälle wie den obigen, daß z.B. ein TouchHandler ein Objekt behalten kann, was sonst bereits gelöscht wäre.
Wieso genau muss der TouchHandler das so machen?
-
void retain();
Kommst du von Objective-C?
Gibt es noch eine Möglichkeit, auf die ich noch nicht gekommen bin?
-
shared_ptr
, und die Engine merkt sich dann nenweak_ptr
auf das Objekt das Input gecaptured hat. Wenn das Objekt in der Zwischenzeit gelöscht wurde kommt beiweak_ptr::lock
dann ein leerershared_ptr
zurück. -
Jedes Objekt hat nen "Deleting" Event, und die Engine hängt sich auf den "Deleting" Event des Input-Empfänger Objekts drauf.
-
Globale Objekt-Registry die jedem Objekt eine (nicht recyclebare) ID zuordnet (mit 64 Bit IDs sollte es ausreichend lange dauern bis die IDs "ausgehen").
-
-
314159265358979 schrieb:
cdtgfvhbj schrieb:
was ist daran schlecht?
Was ist daran gut? Intrusives refcounting hat nur Nachteile gegenüber shared_ptr. Der gröbste dürfte wohl sein, dass es nicht automatisch funktioniert.
Und wenn du weniger dogmatischen Blödsinn schreiben würdest, würden wir dich vielleicht irgendwann ernst nehmen.
-
Und wenn ich Recht habe?
-
Dann hab das mal und wir können weiterreden.
-
Wenn du Recht hast wäre es nett das auch zu begründen. Nur mit einem "das ist Müll" kann man leider wenig anfangen.
hustbaer schrieb:
Kommst du von Objective-C?
Zum Glück nicht, aber ich arbeite derzeit mit einem Game-Framework, das von Objective C portiert wurde und habe gewohnheitsmäßig die Benamung beibehalten.
Evtl schau ich mir die shared_ptr nochmal an, nachdem ich die Input-Handling Geschichte fertig habe. Wie gesagt, ich hatte die Überlegung anfangs schon gemacht, gerade weil shared_ptr automatisch funktioniert, müsste mich aber auch erst mal richtig damit auseinandersetzen, da ich z.B. nicht weiß wie das z.B. mit Vererbung klappt. Zudem war ich mir auch nicht sicher, ob ich die Library unter Android Native zur Verfügung habe, wofür ich das hauptsächlich schreiben will.
-
314159265358979 schrieb:
Und wenn ich Recht habe?
Wenn du Recht hättest, was in diesem Fall (wieder mal) nicht so ist, dann wäre es dennoch sinnloser Blödsinn einfach nur dogmatisch Dinge zu behaupten.
Wir halten dich nämlich tendenziell eher für doof und lästig anstatt dir Dinge die du einfach nur behauptest einfach so zu glauben.
(Also ich kann natürlich nur für mich sprechen, hab aber so das Gefühl als ob ich da nicht ganz allein wäre)
-
Widerleg halt meine Aussage, ansonsten hab ich eben Recht. :p
-
Glaub was du willst.
-
lass TouchReceiver auch von SharedObject ableiten,
-
314159265358979 schrieb:
cdtgfvhbj schrieb:
was ist daran schlecht?
Was ist daran gut? Intrusives refcounting hat nur Nachteile gegenüber shared_ptr. Der gröbste dürfte wohl sein, dass es nicht automatisch funktioniert.
Wenn man glaubt, dass Intrusive Pointer durch Shared Poniter austauschbar sind, hat man wohl keine guten Argumente und, wie hustbaer schon sagte, nichts verstanden.
-
Intrusives Refcounting ist nicht nur durch shared_ptr austauschbar, mit shared_ptr kanns auch weak_ptr geben. Und wie schon gesagt ist manuelles Refcounting absoluter Käse. Ein release vergisst man schnell mal und bei Exception Safety brauchen wir gar nicht erst anfangen.