dynamic_cast böse
-
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.
-
Ich sehe nicht, wie diese parellele pointer haltung sinn machen soll - hab ich 27 verschiedene vectoren statt ein einfaches virtual void update = 0 das ich einfach überall drüberschmeißen kann. In 95% der Fälle braucht man da gar keine casts (und tatsächlich mache ich das so, und hab de facto momentan 0 dynamic_casts). Das ist für mich Kapselung - die Hauptschliefe weiß einfach gar nichts über die details der verhandenen Objekte, und es INTERESSIERT sie auch nicht. Bei deiner Lösung muss ich im Zweifelsfall auch jedesmal die Hauptschleife ändern, wenn ich neue Objekttypen einführe. Neuen vector erstellen, daten da neu einfügen, alles nochmal extra machen. Selbst wenn man das generisch recht einfach machen kann. Irgendwie kommt mir das etwas nach "dynamic_cast vermeiden als reiner selbstzweck" vor. Ob ich jetzt 2 container hab AIObject und OtherObject, oder einen Object, bei dem ich dynamic_cast mache ändert doch NICHTS an der Kapselung, was doch eigentlich das Hauptargument war. Dazu brauch ich in der single-variante in 95% der Fälle eben nichtmal diesen dynamic_cast (oder eben typeid), während ich bei 2 oder mehr vectoren sie nicht nur in 5% der Fälle unterschiedlich behandele, sondern in 100%. Vorallem behandle ich das ganze an Stellen unterschiedlich, wo es völlig unnötig ist. Ich verstehe ja durchaus, das dynamic_cast wegen Kapselung ein prinzipielles Problem hat, aber "ich behandel von vorneherein alles unterschiedlich, hab also gar nicht mehr das Problem das ich manche Dinge in seltenen Fällen auch unterschiedlich behandeln muss" seh ich nicht gerade als Lösung. Hat ein bißchen was von "das Baby mit dem Badewasser ausschütten". Nur um klarzustellen - wenn sich 2 Dinge tatsächlich in den meisten Fällen unterschiedlich Verhalten, halte ich 2 oder mehr vectoren auch für die natürliche Lösung. Das ist aber in dem Beispiel eben nicht der Fall.
-
Warum müssen in-game Objekte unbedingt durch c++ Objekte repräsentiert werden?
Wie soll man eine Hierarchie vernünftig aufbauen (um dynamic_cast verwenden zu können), wenn man z.B. solche Objekte hat:Lastwagen: beweglich, unbewaffnet
Panzer: beweglich, bewaffnet
Geschützturm: unbeweglich, bewaffnetGameObjekt -> Beweglich -> Lastwagen
Beweglich -> Bewaffnet -> Panzer
GameObjekt -> ???? -> GeschützturmAbgesehen davon, wie würdet ihr etwas globales, z.B. Physik, implementieren? In update() durch die Liste der Objekte gehen, schauen ob sie kollidieren können (also dynamic_cast), und dann auf Kollision überprüfen?
Wie sieht Interaktion zwischen 2 Objekten über ein Interface wieclass GameObjekt { public: virtual void update() = 0; };
aus?