dynamic_cast böse
-
Wenn jemand einen Compiler oder etwas vergleichbares ohne explizite Typabfragen kennt, immer her damit. Die meisten sind ja im prozeduralen oder funktionalen Stil und haben diesbezüglich natürlich wenig Hemmungen, aber selbst die wenigen objektorientierten Compiler, die ich gesehen habe, machen sowas.
-
Der Epoch-Compiler kommt ohne aus. Also in der Compilerlogik selbst; den Code, den der Compiler übersetzt, muss der Compiler natürlich ganz anders behandeln.
-
seldon schrieb:
Der Epoch-Compiler kommt ohne aus.
Läßt der auch einen Optimierer über den Baum laufen?
-
Exakt. Ein dynamic_cast tritt eigentlich praktisch immer als Symptom eines Widerspruchs in deiner Abstraktion auf. Denn überleg mal: dynamic_cast (oder auch typeid, sei es nun selbstgebaut oder nicht) bedeutet doch, dass du Objekte eines konkreten Typs an einer Stelle, an der deine Abstraktion eigentlich vorsieht, dass alle Objekte unabhängig ihres konkreten Typs gleich behandelt werden sollen, anders behandeln willst. Sowas sollte schwer zu denken geben...
Mal ne Verständnisfrage, wenn ich code habe der zu 95% die Vererbungshirarchie "normal" verwendet (Beispiel: Objekte in einem Spiel, die nach unten hin mehr ausdifferenziert werden, von "du bist in der welt" zu "du wirst in der welt animiert" zu "du bist für die physik relevant" zu "du kannst dich selbst bewegen")
Jetzt will ich alle Objekte (z.B. zu test/debugging zwecken) eines bestimmten subtyps entfernen. Wie würdet ihr sowas ohne dynamic_cast/typeid - oder ersatz - machen? Ich seh da keine sinnvolle andere Möglichkeit. Ideen? (Nicht das ich den Fall in aktuellem code hätte, bin nur neugierig)Ein anderer Fall, Vererbung wie oben, ich würde ein spezifisches Objekt aus dem container holen (über ne map) und obwohl ich eigentlich "weiß" was für ein subtyp das ist - ich kenn das objekt das ich hole - will ich damit der code robuster ist das nochmal prüfen. (z.B. ich will was an der animation ändern, und will nur sichergehen, dass es stimmt - oder "schlimmer", diese funktion kann über lua aufgerufen werden, und um den lua-nutzer nicht zu erlauben das programm zu crashen müsst ich prüfen)
De facto is das einzige dynamic_cast das ich je benutzt hab absolut sinnlos gewesen, aber die paar Fragen hab ich irgendwie doch.
-
kleiner Troll schrieb:
Jetzt will ich alle Objekte (z.B. zu test/debugging zwecken) eines bestimmten subtyps entfernen. Wie würdet ihr sowas ohne dynamic_cast/typeid - oder ersatz - machen? Ich seh da keine sinnvolle andere Möglichkeit. Ideen? (Nicht das ich den Fall in aktuellem code hätte, bin nur neugierig)
Ich würde dafür sorgen, dass diese Objekte gar nicht erst "eingefügt" werden...
kleiner Troll schrieb:
Ein anderer Fall, Vererbung wie oben, ich würde ein spezifisches Objekt aus dem container holen (über ne map) und obwohl ich eigentlich "weiß" was für ein subtyp das ist - ich kenn das objekt das ich hole - will ich damit der code robuster ist das nochmal prüfen. (z.B. ich will was an der animation ändern, und will nur sichergehen, dass es stimmt - oder "schlimmer", diese funktion kann über lua aufgerufen werden, und um den lua-nutzer nicht zu erlauben das programm zu crashen müsst ich prüfen)
Das versteh ich nicht so ganz.
-
Das erste ist keine option - ich will die Objekte ja drinnen haben, bis ich z.B. console öffne und "Remove Enemies" oder so eingebe. Gar nicht erst einfügen heißt ja, ich habe nie "Enemies" in meiner Spielwelt - das halte ich für schwachsinn.
Letzeres ein konkretes Beispiel:
SetAI soll eine lua-funktion sein die den Namen eines Objekts nennt, und die Kampf-KI dafür einstellt
SetAI("Oberbösewicht", "Aggressive")
Bedingung ist natürlich, dass das benannte Objekt überhaupt eine Kampf-KI hat.
//map<string, Object*> objectMap; CombatObject* co = dynamic_cast<CombatObject*>(objectMap["Oberbösewicht"]); if(co) { SetAI("Aggressive"); } else { lua_error("Dummer nutzer, 'Oberbösewicht" kann doch gar nicht kämpfen!") }
-
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?
kleiner Troll schrieb:
SetAI soll eine lua-funktion sein die den Namen eines Objekts nennt, und die Kampf-KI dafür einstellt
Und warum ist dann
SetAI()
keine virtuelle Methode derObject
-Klasse? Du könntest z.B. als Default-Verhalten nichts tun und einen Fehler zurückgeben, und fürCombatObject
-Objekte eben die KI setzen.
-
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.
Nexus schrieb:
kleiner Troll schrieb:
SetAI soll eine lua-funktion sein die den Namen eines Objekts nennt, und die Kampf-KI dafür einstellt
Und warum ist dann
SetAI()
keine virtuelle Methode derObject
-Klasse? Du könntest z.B. als Default-Verhalten nichts tun und einen Fehler zurückgeben, und fürCombatObject
-Objekte eben die KI setzen.Ja, so in der Richtung.
-
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.
Im Allgemeinen ist das ein Problem, das als Expression Problem bekannt ist. OOP macht es einfach, neue Subtypen hinzufügen, und schwer, neue Operationen hinzuzufügen. Wenn man explizite Fallunterscheidungen für die Subtypen hat, ist es genau umgekehrt. Man kann auch das Visitor-Pattern verwenden, um das Problem anders zu verlagern, damit ist es dann wieder leicht, neue Operationen einzuführen, und schwer, neue Subtypen hinzuzfügen. Wenn man die Fixkosten des Visitor-Patterns tragen will.
Ich habe in der Regel mit Problemen zu tun, bei denen eher neue Funktionen als neue Subtypen hinzukommen. Deshalb hab ich das mit den Compilern erwähnt. Ich bin auch aktiv auf der Suche nach Compilern, die in dem Punkt elegante Lösungen bieten. Epoch hab ich mir noch nicht angesehen, aber das werde ich garantiert noch tun.
-
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!?