dynamic_cast böse
-
volkard schrieb:
ButterFisch schrieb:
Wenn ja, habt ihr irgendwas ähnliches gebaut, wie z.B. getObjectType?
Ja.
Diese Antwort wundert mich schon
Wann hast du denn so etwas Ähnliches mal gebaut?
Mir fallen da fast keine (meiner Ansicht nach) sinnvollen Anwendungsfälle ein.
Ausnahmen sind natürlich Fälle, wo man wirklich einen String, int oder so direkt brauch wie z. B. Xml-Serialisierung.
Aber ich denke nicht, dass ButterFisch das meinte.
-
Das würde die Hauptschleife zerhacken. Nein.
Seh ich genauso.
Naja, wie wärs mit einer Methode isCombatant()? Ich unterstelle mal, dass du bei einem Spiel schon beim Design der Basisklasse vorhersehen kannst, dass es kämpfende und nicht kämpfende Figuren gibt.
Hab ich ich mir auch überlegt. Sobald du aber mehrere solcher "Spezialfälle" hast, müllt man sich das interface zu (nur um mal anzufangen, ist animiert/ist nur hintergrund, kann man damit agieren/kann man nicht, kann es sich selbst bewegen/kann es nicht... etc) - und wirklich gekapselt ist dann eigentlich auch nichts mehr. Man kann sich eventuell da grundlegend ein besseres design überlegen, nur welches? Für mich zufriedenstellende antworten hab ich da noch keine.
-
volkard schrieb:
Das würde die Hauptschleife zerhacken. Nein.
Nicht zwingend. Man könnte ja immer noch einen
std::vector<Object*>
mit Verweisen auf die eigentlichen Elemente haben. Oder Ranges einsetzen. Ist zwar zusätzlicher Synchronisierungsaufwand, den man aber auch wegkapseln könnte.Bashar schrieb:
Ich habe in der Regel mit Problemen zu tun, bei denen eher neue Funktionen als neue Subtypen hinzukommen.
Im Beispiel hier ist der Vorteil über
dynamic_cast
zwar begrenzt, aber ich habe mich vor einiger Zeit von Alexandrescu inspirieren lassen und ein System entwickelt, mit dem man quasi virtuelle Funktionen nicht-intrusiv (d.h. ausserhalb der Klasse) definieren kann. Das Ganze hat zwei Nachteile: Aufrufe sind wegen des zusätzlichen Dispatchings langsamer und für Derived-To-Base-Konvertierungen muss die Klassenhierarchie explizit nochmals angegeben werden, da C++ keine Reflection kennt. Aber teilweise ist das den Nutzen wert. Interessant wirds vor allem bei zwei Argumenten (Double Dispatch).// Beispielhierarchie class Base { public: virtual ~Base() {} }; class Derived1 : public Base {}; class Derived2 : public Base {}; // Freie Funktionen für abgeleitete Typen void Func(Derived1* d); void Func(Derived2* d); // Funktionen registrieren thor::SingleDispatcher<Base*> dispatcher; dispatcher.Register<Derived1>(&Func); dispatcher.Register<Derived2>(&Func); // Funktion aufrufen Base* ptr = new Derived1; dispatcher.Call(ptr); // Aufruf: void Func(Derived1* d); delete ptr;
Alexandrescu hat in seinem Buch auch einen Acyclic-Visitor vorgestellt, der geht in eine ähnliche Richtung.
-
XSpille schrieb:
Ausnahmen sind natürlich Fälle, wo man wirklich einen String, int oder so direkt brauch wie z. B. Xml-Serialisierung.
Aber ich denke nicht, dass ButterFisch das meinte.Doch, auch sowas.
-
XSpille schrieb:
Mir fallen da fast keine (meiner Ansicht nach) sinnvollen Anwendungsfälle ein.
Kann man neben Serialisierung auch für eine Typ-ID verwenden, die etwas mehr kann als
std::type_info
. Z.B. die eine Integer-Repräsentation besitzt, was wiederum für schnelles Dynamic Dispatch von grossem Vorteil sein kann.Aber ja, braucht man recht selten.
-
XSpille schrieb:
volkard schrieb:
ButterFisch schrieb:
Wenn ja, habt ihr irgendwas ähnliches gebaut, wie z.B. getObjectType?
Ja.
Diese Antwort wundert mich schon
Wann hast du denn so etwas Ähnliches mal gebaut?Ausdrücke optimieren. Sie Summe kann zum Beispiel, falls beide Summanden Konstanten sind, in ihrer optimize-Funktion eine neu erzeugte Konstante zurückgeben. Dazu muß die Summe sich die Typen der Summanden anschauen dürfen.
RTTI oder dynamic_cast sind mir da zu groß und unberechenbar.
virtual Ausdruck* Ausdruck::getPtrToSumme(){return nullptr); virtual Summe* Summe::getPtrToSumme(){return this);
Oder so ähnlich.
-
volkard schrieb:
XSpille schrieb:
volkard schrieb:
ButterFisch schrieb:
Wenn ja, habt ihr irgendwas ähnliches gebaut, wie z.B. getObjectType?
Ja.
Diese Antwort wundert mich schon
Wann hast du denn so etwas Ähnliches mal gebaut?Ausdrücke optimieren. Sie Summe kann zum Beispiel, falls beide Summanden Konstanten sind, in ihrer optimize-Funktion eine neu erzeugte Konstante zurückgeben. Dazu muß die Summe sich die Typen der Summanden anschauen dürfen.
RTTI oder dynamic_cast sind mir da zu groß und unberechenbar.
virtual Ausdruck* Ausdruck::getPtrToSumme(){return nullptr); virtual Summe* Summe::getPtrToSumme(){return this);
Oder so ähnlich.
Was spricht bei einer einfachen Summe gegen ein double dispatch?
-
Nexus schrieb:
XSpille schrieb:
Mir fallen da fast keine (meiner Ansicht nach) sinnvollen Anwendungsfälle ein.
Kann man neben Serialisierung auch für eine Typ-ID verwenden, die etwas mehr kann als
std::type_info
. Z.B. die eine Integer-Repräsentation besitzt, was wiederum für schnelles Dynamic Dispatch von grossem Vorteil sein kann.Verstehe...
Ich würde das aber eher als Mechanismus zur Reduzierung der notwendigen Funktionen bei einem dynamic dispatch ansehen als als dynamic-cast-'Ersatz'
-
XSpille schrieb:
Was spricht bei einer einfachen Summe gegen ein double dispatch?
Summe ist keine Funktion, sondern eine Klasse.
Eine Sorte von Knoten im Syntaxbaum.
Double-Dispatch könnte ich mir beim Erzeugen einer Summe vorstellen. Aber nicht beim nachträglichen Optimieren. Außerdem habe ich drei beteiligte Typen, müßte Triple-Dispatch machen. Ich fürchte, das würde mir den Code zu weit auseinanderhacken.
-
volkard schrieb:
XSpille schrieb:
Was spricht bei einer einfachen Summe gegen ein double dispatch?
Summe ist keine Funktion, sondern eine Klasse.
Eine Sorte von Knoten im Syntaxbaum.
Double-Dispatch könnte ich mir beim Erzeugen einer Summe vorstellen. Aber nicht beim nachträglichen Optimieren. Außerdem habe ich drei beteiligte Typen, müßte Triple-Dispatch machen. Ich fürchte, das würde mir den Code zu weit auseinanderhacken.Verstanden
Aber eigentlich ist es ja eher eine isPropertyFulfilled als ein getObjectType
-
Nexus schrieb:
Warum keinen separaten Container für Gegner?
Exakt das wäre die Lösung.
volkard schrieb:
Nexus schrieb:
kleiner Troll schrieb:
Gar nicht erst einfügen heißt ja, ich habe nie "Enemies" in meiner Spielwelt - das halte ich für schwachsinn.
Nein, das impliziert nicht, dass du keine Gegner haben kannst. Aber du sollst nicht alles in einen einzelnen Container werfen, wenn du nachher wieder Fallunterscheidungen benötigst. Warum keinen separaten Container für Gegner?
Das würde die Hauptschleife zerhacken. Nein.
Wieso!?
-
dot schrieb:
volkard schrieb:
Nexus schrieb:
kleiner Troll schrieb:
Gar nicht erst einfügen heißt ja, ich habe nie "Enemies" in meiner Spielwelt - das halte ich für schwachsinn.
Nein, das impliziert nicht, dass du keine Gegner haben kannst. Aber du sollst nicht alles in einen einzelnen Container werfen, wenn du nachher wieder Fallunterscheidungen benötigst. Warum keinen separaten Container für Gegner?
Das würde die Hauptschleife zerhacken. Nein.
Wieso!?
foreach(ding in welt) ding->tuwas(); foreach(ding in welt) ding->zeichneDich();
Das wird ja unter Umständen zu
foreach(ding in gegnerEinheiten) ding->tuwas(); foreach(ding in eigeneEinheiten) ding->tuwas(); foreach(ding in gegnerGebäude) ding->tuwas(); foreach(ding in eigeneGebäude) ding->tuwas(); ...
-
Wenn das passiert, dann hat man was falsch gemacht. Denn eigentlich sollte die Methode tuwas() wohl nicht in all diesen Interfaces auftreten, sondern all diese Objekte das Interface mit der tuwas() Methode implementieren...
-
dot schrieb:
Wenn das passiert, dann hat man was falsch gemacht. Denn eigentlich sollte die Methode tuwas() wohl nicht in all diesen Interfaces auftreten, sondern all diese Objekte das Interface mit der tuwas() Methode implementieren...
Verstehe ich nicht. Vielleicht war .tuwas() zu abstrakt.
Nimm .zeichneDichAufDenBildschirm() statt .tuwas().
-
Das ändert doch nichts an meinem Argument
Wenn ich alle Dinge die sich zeichnen sollen gleich behandeln will, dann sollen die doch auch bitte alle das Interface Drawable implementieren. Dann hab ich einen Container der alle Drawables enthält und kann alle Drawables zeichnen!? Ob manche der Objekte, die hinter diesen Drawables stehen, auch noch das Interface Player, Enemy oder Cake implementieren, ist für den Zeichencode doch irrelevant!?
-
dot schrieb:
Das ändert doch nichts an meinem Argument
Wenn ich alle Dinge die sich zeichnen sollen gleich behandeln will, dann sollen die doch auch bitte alle das Interface Drawable implementieren. Dann hab ich einen Container der alle Drawables enthält und kann alle Drawables zeichnen!?Alle können zeichneDich() und alle können tuDenNächstenSchrittAnhandDeinerKI(). Und mache davon sind Gegner.
Also alle liegen in zwei Containern und manche sogar in drei.
Willst Du mir jetzt vorschlagen, daß ich für jedes Interface einen nichtbesitzenden Zeiger-Container anlege? Und noch einen vierten besitzenden Zeiger-Container?
-
volkard schrieb:
dot schrieb:
Das ändert doch nichts an meinem Argument
Wenn ich alle Dinge die sich zeichnen sollen gleich behandeln will, dann sollen die doch auch bitte alle das Interface Drawable implementieren. Dann hab ich einen Container der alle Drawables enthält und kann alle Drawables zeichnen!?Alle können zeichneDich() und alle können tuDenNächstenSchrittAnhandDeinerKI(). Und mache davon sind Gegner.
Also alle liegen in zwei Containern und manche sogar in drei.
Willst Du mir jetzt vorschlagen, daß ich für jedes Interface einen nichtbesitzenden Zeiger-Container anlege? Und noch einen vierten besitzenden Zeiger-Container?Ja, das ist imo die natürliche Lösung (wobei all diese Container natürlich nur die entsprechenden Interface-Pointer enthalten und nicht die Objekte an sich). Denn der Renderer arbeitet mit völlig anderern Datenstrukturen wie die KI oder die Spiellogik. Die Dinge, die in einem Frame gerendert werden sollen, liegen vielleicht in einem vector, für ein möglichst effizientes Traversal. Die Spiellogik hält die Gegner aber doch besser in einem Grid, um möglichst schnell die Gegner im Umkreis des Spielers finden zu können. Die KI wird ihre Agenten vielleicht durch ein Navigation-Mesh schicken wollen.
Alle Objekte in den selben Container zu stopfen mach hier von vornherein keinen Sinn.
So eine Gameloop, wo einfach alle Objekte dies im Spiel gibt das selbe Interface besitzen und ständig Update() und Draw() aufgerufen wird, funktioniert vielleicht für Pong und Tetris. Aber spätestens bei Super-Mario ist Schluss...
-
dot schrieb:
Ja, das ist imo die natürliche Lösung. Denn der Renderer arbeitet mit völlig anderern Datenstrukturen wie die KI oder die Spiellogik.
Der Renderer kann ja andere Datenstrukturen haben. Und ein Reiter kann sein Reiterbild im Renderer kennen. Unwichtige Details.
dot schrieb:
Die Dinge, die in einem Frame gerendert werden sollen, liegen vielleicht in einem vector, für ein möglichst effizientes Traversal. die Spiellogik hält die Gegner aber doch besser in einem Grid, um möglichst schnell die Gegner im Umkreis des Spielers finden zu können. Die KI wird ihre Agenten vielleicht durch ein Navigation-Mesh schicken wollen.
Nichtbesitzende Zeigercontainer für beschleinigten Zugriff. Das ist auch eine ganz andere Ebene.
dot schrieb:
Alle Objekte in den selben Container zu stopfen mach hier von vornherein keinen Sinn.
Da bin ich halt ganz anderer Meinung.
-
volkard schrieb:
dot schrieb:
Alle Objekte in den selben Container zu stopfen mach hier von vornherein keinen Sinn.
Da bin ich halt ganz anderer Meinung.
Und was genau sind da die Argumente dafür? Genau damit bringt man sich doch erst in die Situation, wo man ständig irgendwelche Typabfragen und Downcasts braucht!?
Ich halt es jedenfalls für eine notwendige Eigenschaft von gutem Design, dass die Dinge völlig natürlich ineinandergreifen und das Typsystem der Sprache als Ausdrucksmittel dient und nicht ständig dagegen gekämpft oder gar ein eigenes erfunden werden muss...
-
dot schrieb:
Und was genau sind da die Argumente dafür?
Es klappt auch.
dot schrieb:
Genau damit bringt man sich doch erst in die Situation, wo man ständig irgendwelche Typabfragen und Downcasts braucht!?
Nicht ständig.
dot schrieb:
Ich halt es jedenfalls für eine notwendige Eigenschaft von gutem Design, dass die Dinge völlig natürlich ineinandergreifen und das Typsystem der Sprache als Ausdrucksmittel dient und nicht ständig dagegen gekämpft oder gar ein eigenes erfunden werden muss...
Du übertreibst.