dynamic_cast böse



  • 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 der Object -Klasse? Du könntest z.B. als Default-Verhalten nichts tun und einen Fehler zurückgeben, und für CombatObject -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 der Object -Klasse? Du könntest z.B. als Default-Verhalten nichts tun und einen Fehler zurückgeben, und für CombatObject -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!?



  • 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?


Anmelden zum Antworten