dynamic_cast böse



  • Was spricht an der Stelle gegen ein Template und ein wenig TMP Magie?
    Den Typ musst du ja eh angeben und den Cast konkret hinschreiben.

    template <unsigned ComponentType>
    typename tmp_get_component_type<ComponentType>::type
    * getComponent()
    {
      return static_cast<typename tmp_get_component_type<ComponentType::type *>(getComponent(ComponentType));
    }
    

    Dabei bildet die MetaFunktion tmp_get_component_type den unsiged (enum) auf den dazugehören konkreten ComponentType ab.



  • cooky451 schrieb:

    std::map<ComponentType, std::unique_ptr<Component>> components_;
    

    Quark.
    http://en.wikipedia.org/wiki/Inner-platform_effect



  • dot schrieb:

    pumuckl schrieb:

    [...] Es handelt sich ausschließlich um die Rückgabe einer abstrakten generischen Fabrik, bei der ich in der Anwendung erstens weiß, dass sie ein Objekt eines bestimmten konkreten Typs zurückgibt (weil ich es direkt vorher angefordert habe) und zweitens eine spezielle Methode dieses konkreten Typs brauche, die nur dieser eine Typ hat[...]

    Wieso gibt die Fabrik dann nicht einfach gleich ein Objekt des konkreteren Typs zurück?
    Wenn du an der Stelle die konkrete Fabrik kennen würdest, könntest du ganz einfach einen kovariaten Rückgabetyp verwenden und bist komplett statisch Typsicher.

    Es handelt sich nicht um eine Fabrik im "Objektorientierten" Sinne mit Laufzeitpolymorphismus etc., sondern um ein kleines Template-Biest auf Basis von std::function's an Stelle der konkreten Fabrik, gewürzt mit optionaler pre/post-construction/destruction Policy. Zur Veranschaulichung mal ein bisschen Code:

    template <class Signature, class DefaultCreator, class DefaultDeleter, class ManagementPolicy = detail::NoManagemetPolicy>
    class GenericAbstractFactory : public ManagementPolicy
    {
      typedef typename boost::function_types::result_type< Signature >::type ResultType;
      typedef typename boost::mpl::if_<
        typename std::is_pointer<ResultType>::type, 
        ResultType, 
        typename std::add_reference<ResultType>::type
      >::type DeleterArg;
    
    public:
      typedef Signature CreatorSignature;
      typedef void DeleterSignature(DeleterArg);
      typedef std::function<CreatorSignature> Creator;
      typedef std::function<DeleterSignature> Deleter;
    
    private:
      Creator theCreator;
      Deleter theDeleter;
    
    public:
      void replace(Creator const& newCreator, Deleter const& newDeleter)
      {
        theCreator = newCreator;
        theDeleter = newDeleter;
      }
    
      void restore()
      {
        replace(DefaultCreator(), DefaultDeleter());
      }
    
      ResultType create(/* passende Argumente... */)
      {
          ManagementPolicy::preCreation();                         
          ResultType r = theCreator(/* argumente */);
          ManagementPolicy::postCreation(r); 
          return r; 
      }
    
      void destroy(DeleterArg r)
      {
        ManagementPolicy::preDestruction(r);
        theDeleter(r);
        ManagementPolicy::postDestruction();
      }
    };
    

    Benutzung etwa wie folgt:

    //communicatorfactory.h
    struct DefaultCommunicatorCreator
    {
      ClientCommunicator* operator()(ServerEventCallback const& serverEventCallback, ConnectionType connType)
      {
        switch (connType.underlying())
        {
        case ConnectionType::LOCAL:
          return new LocalClientCommunicator(serverEventCallback);
        /* ... */
        }
      }
    };
    
    typedef util::GenericAbstractFactory<
      ClientCommunicator*(communication::ServerEventCallback const&, communication::ConnectionType), 
      DefaultCommunicatorCreator, 
      std::default_delete<ClientCommunicator>, 
      util::SingleProductPolicy<ClientCommunicator>
    >
    CommunicatorFactory;
    
    /* ... */
    
    //install logging
    CommunicatorFactory::instance().replace(
      [&myLogger](communication::ServerEventCallback const& sec, communication::ConnectionType ct) 
      {
        myLogger << "Creation!\n";
        DefaultCommunicatorCreator dcc;
        return dcc(sec, ct);
      },
      [&myLogger](ClientCommunicator* cc)
      {
        myLogger << "Deletion!\n";
        delete cc;
      });
    
    ClientCommunicator* myCc = CommunicatorFactory::instance().create(/* ... */); //logged
    CommunicatorFactory::instance().destroy(myCc); //logged
    
    CommunicatorFactory::instance().restore(); //return to default creation
    

    Kurz: Es gibt nur die eine Factory, sie wird nicht durch eine konkrete Fabrik ersetzt, sondern lediglich die Erzeugerfunktion wird ersetzt. Der Rückgabetyp ist immer gleich.



  • Ja gut, wenn man es unbedingt genau so machen muss, dann geht das natürlich nicht. Ich geh mal davon aus, dass du verdammt gute Gründe hast das so zu machen 😉



  • dot schrieb:

    Ich geh mal davon aus, dass du verdammt gute Gründe hast das so zu machen 😉

    Verschiedene - ob sie so verdammt gut sind, mag ich nicht beurteilen 😉
    So kann ich wie angedeutet zum Beispiel diverse Mechanismen relativ einfach einschleusen, ohne jedesmal ein ganzes Klasseninterface neu implementieren zu müssen. (Hab ich schonmal erwähnt, dass ich überzeugt bin, dass vieles, was bisher mit Laufzeitpolymorphie und einer Basisklasse ohne Membervariablen und ein oder zwei virtuellen Funktionen implementiert wurde, künftig mit Funktionsobjekten und lambdas implementiert wird? ;))
    Meine Unit-Tests ersetzen zum Beispiel die Funktoren durch Mocks aus einem Mocking-Framework, so kann ich sicherstellen, dass die Factory so aufgerufen wird, wie ichs erwarte, und genau das zurückliefern, was ich grade benötige - nämlich meistens auch Mocks.
    Ich benutze mehrere Factories in meinem Projekt, die zwar verschiedene Signaturen und Produkt-Typen haben, aber gleiche/ähnliche Management-Policies benutzen und auch das Replacement der Creator/Deleter-Funktoren benötigen.

    Zu guter Letzt: selbst wenn ich das gute alte objektorientierte Abstract Factory Pattern bemüht hätte, müsste ich immernoch auf die abstrakte Fabrik zugreifen oder aber per dynamic_cast die konkrete Fabrik herauszaubern - der Cast würde nur auf eine andere Ebene verlagert.



  • aber per dynamic_cast die konkrete Fabrik herauszaubern - der Cast würde nur auf eine andere Ebene verlagert.

    Um das hochcasten kommst immer dann nicht drumherum, wenn du spezialisierte Schnittstellen durch generisches tarrain transportieren musst.

    Plugins sind da wohl der Präzedenz-Fall ...

    Nur zieht das nicht zwangslauefig nen dynamic cast hinter sich ... im gegenteil, meist wuerde das nicht mal funktionieren.

    Man baut oft Schnittstellen mehrstufig, und am ende hat man immer noch die Implementation ...

    Beispiel:

    generische Schnittstelle: IPlugin
    Spezialisierte Schnittstelle: IMyViewIrgendwasPlugin
    ImplementationsKlasse: XYZPluginMyViewImpl

    Oft ist nicht ma IMyViewIrgendwasPlugin von IPlugin abgeleitet, sondern man macht nen Schnittstelenwechsel in dem man sich nen generischen Zeiger auf das interface geben laesst, meist vom typ void * und dann sofort hochcastet auf das richtige Interface, aber nicht auf die Implmentation.
    Mit dynamic_cast kannst sowas gar ned machen ...

    Auf deine eigene Typcodierung musst dich aber wiederum schon verlassen koennen (sonst gibts datenmuell, iss aber auch klar). Warum dann also RTTI dazu verwenden ?

    Ciao ...



  • pumuckl schrieb:

    So kann ich wie angedeutet zum Beispiel diverse Mechanismen relativ einfach einschleusen, ohne jedesmal ein ganzes Klasseninterface neu implementieren zu müssen.

    Du könntest aber auch deine Factory von einem allgemeineren Factory-Interface ableiten:

    template <typename T, typename... Args>
    class IFactory
    {
    public:
      virtual T* create(Args... args);
    };
    

    Deine Generatoren könnten dann eine konkrete Instanz liefern und die create Methode der konkreten Factory einen kovarianten Returntype haben. An Stellen wo die konkrete Factory bekannt ist, kennst du so komplett statisch Typsicher und ganz ohne irgendeinen cast die erzeugten Objekte über ihren konkreten Typ, wobei du immer noch dieses Template und deinen Creator und Deleter verwenden kannst. Nachdem dann alles statisch Typsicher ist, brauchst du dann auch keine Tests mehr, die überprüfen ob die dynamischen Typen den Erwartungen entsprechen.

    pumuckl schrieb:

    Zu guter Letzt: selbst wenn ich das gute alte objektorientierte Abstract Factory Pattern bemüht hätte, müsste ich immernoch auf die abstrakte Fabrik zugreifen oder aber per dynamic_cast die konkrete Fabrik herauszaubern - der Cast würde nur auf eine andere Ebene verlagert.

    Der Punkt ist doch, dass du die konkrete Fabrik kennen musst. Wenn du nur die abstrakte Fabrik kennst, dann hast du imo (aus bereits genannten Gründen) von vornherein einen Widerspruch in deinem Design.



  • volkard schrieb:

    cooky451 schrieb:

    std::map<ComponentType, std::unique_ptr<Component>> components_;
    

    Quark.
    http://en.wikipedia.org/wiki/Inner-platform_effect

    Hm. Was schlägst du als Verbesserung vor? Oder würdest du weiter ein OOP Modell bevorzugen?



  • cooky451 schrieb:

    volkard schrieb:

    cooky451 schrieb:

    std::map<ComponentType, std::unique_ptr<Component>> components_;
    

    Quark.
    http://en.wikipedia.org/wiki/Inner-platform_effect

    Hm. Was schlägst du als Verbesserung vor? Oder würdest du weiter ein OOP Modell bevorzugen?

    Du machst hier einen Baukasten, der Layering ermöglicht, falls ich den Sinn recht verstehe, obwohl das viel besser als Sprachmittel schon da ist.
    Was ich als Verbesserung vorschlage? Na, Löschen natürlich.



  • Erstmal zu einer Implementierung von sowas wie der entity Klasse:
    Das kann man noch etwas sauberer machen, indem man type erasure verwendet. Ich selbst habe eine Klasse die dieses interface anbietet:

    class entity
    {
        template< class Type >
        bool add_component( Type && ); //false falls Type schon in entity
        template< class Type >
        Type *query(); //gibt pointer auf die interne Instanz von Type zurück (oder 0)
    };
    ////////////////////////////////////////////////////////////////////
    class A{};
    entity ent;
    ent.add_component( A() );
    ent.query< A >();
    

    Allerdings ist das nicht Sinn eines entity systems.
    Ich speichere jeden Komponententyp in einem seperaten Container ab (nicht nur Pointer, die eigentlichen Instanzen). Jedes Komponentobjekt hat zusätzlich einen Pointer auf eine Struktur, die nicht mehr beinhaltet als eine id und Referenzzähler und damit die entity repräsentiert. Jeder id können so mehrer Komponenten zugeordnet sein (aber immer nur 1 je Typ).
    Nachdem eine entity zum entity system hinzugefügt wurde, übergebe ich jedem Komponenten eine Klasse, in der Pointer auf alle zur entity gehörigen Komponenten per query (s.o.) abgefragt werden können. Die Komponente kann damit anstellen, was sie will, z.B. pointer auf andere Komponenten speichern (die RenderComponent braucht einen pointer auf die TransformComponent, um an der richtigen Stelle gezeichnet werden zu können), oder callbacks in anderen Komponenten setzen (z.B. sich bei onCollision in der TransformComponent registrieren).
    Im mainloop muss jetzt nur jedes sub system einmal geupdatet werden. Der Renderer wird beispielsweise über den Container mit RenderComponent s iterieren und jedes Objekt zeichnen (oder auch nicht, je nachdem). Das PhysicsSystem wird sich darum kümmern, dass auf Kollision überprüft wird und evtl. callbacks aufrufen.
    Man spart sich virtual function calls ohne Ende, und hat zusätzlich verschiedene Aspekte der Simulation voneinander getrennt (Warum sollte Grafik irgendwas mit Input zu tun haben?). Das eröffnet die Möglichkeit, mehr parallel abzuarbeiten.

    volkard schrieb:

    cooky451 schrieb:

    std::map<ComponentType, std::unique_ptr<Component>> components_;
    

    Quark.
    http://en.wikipedia.org/wiki/Inner-platform_effect

    Das sehe ich nicht so. Wie würdest du denn das Problem mit Lastwagen, Panzer und Geschützturm von vorhin mit Polymorphie lösen?
    Außerdem ist rtti viel langsamer als es sein könnte, weil die Compilerbauer Rücksicht auf dlls nehmen. Da kann es sinnvoller sein, sich selbst was zu bauen, vor allem wenn es von außen mindestens genauso gut aussieht (und nichtmal besonders komplex ist).



  • GorbGorb schrieb:

    volkard schrieb:

    Quark.
    http://en.wikipedia.org/wiki/Inner-platform_effect

    ]
    Das sehe ich nicht so. Wie würdest du denn das Problem mit Lastwagen, Panzer und Geschützturm von vorhin mit Polymorphie lösen?

    Also das da:
    Lastwagen: beweglich, unbewaffnet
    Panzer: beweglich, bewaffnet
    Geschützturm: unbeweglich, bewaffnet

    a) bewegeDich() auf Geschütztürmen tut nichts.
    a1) Geschütztürme haben eine Geschwindigkeit von 0
    c) Lastwagen und Panzer erben von BeweglichesDing
    c1) und getBeweglichesDingPtr(){return this;}
    Ach, mir fiele schon was passendes ein, denke ich.

    GorbGorb schrieb:

    Außerdem ist rtti viel langsamer als es sein könnte, weil die Compilerbauer Rücksicht auf dlls nehmen. Da kann es sinnvoller sein, sich selbst was zu bauen,

    Ups, hab ich in c1) ja schon gemacht.

    GorbGorb schrieb:

    vor allem wenn es von außen mindestens genauso gut aussieht (und nichtmal besonders komplex ist).

    Im Gegensatz zu Dir, sehe ich nicht, was durch solche Entities gelöst wird, was sich anders nicht viel besser anfühlen würde. Zum Beispiel denke ich beim Bedarf einer Liste aller beweglichen Objekte zuerst an eine intrusive doppelt verkettete Liste.



  • @volkard
    Hast du das schon mal mit etwas komplexeren Dingen ausprobiert? Bei mir endet das dann so:

    class Object
    {};
    class MovingObject : virtual public Object
    {};
    class VisibleObject : virtual public Object
    {};
    class PhysicalObject : virtual public Object
    {};
    class ActiveObject : virtual public Object
    {};
    ...
    

    Und jedes neue Objekt muss weiter hart gecoded werden. Modulbasiert kannst du die Objekte z.B. einfach aus einem Script parsen - der C++ Code muss nicht mal wissen, was für Objekte es in dem Spiel eigentlich gibt.
    Es geht natürlich auch so, aber wie verhinderst du dann die dynamic_cast Orgie?



  • cooky451 schrieb:

    @volkard
    Hast du das schon mal mit etwas komplexeren Dingen ausprobiert?

    Nein. Komplexere Dinge vermeide ich.



  • cooky451 schrieb:

    class Object
    {};
    class MovingObject : virtual public Object
    {};
    class VisibleObject : virtual public Object
    {};
    class PhysicalObject : virtual public Object
    {};
    class ActiveObject : virtual public Object
    {};
    ...
    

    Ich fürchte, dass diese 4 Unterklassen bei mir eine einzige Klassen wären, die die verschiedenen Möglichkeiten einfach durch interne states kodiert. Ich schätze, dass in dem Fall die Klassen, die eine Kombination dieser 4 sind einfach gegenüber den Spezialfällen überwiegen.



  • volkard schrieb:

    Also das da:
    Lastwagen: beweglich, unbewaffnet
    Panzer: beweglich, bewaffnet
    Geschützturm: unbeweglich, bewaffnet

    a) bewegeDich() auf Geschütztürmen tut nichts.
    a1) Geschütztürme haben eine Geschwindigkeit von 0
    c) Lastwagen und Panzer erben von BeweglichesDing
    c1) und getBeweglichesDingPtr(){return this;}
    Ach, mir fiele schon was passendes ein, denke ich.

    Es läuft am Ende darauf hinaus, dass ein Objekt über Schnittstellen verfügt, mit denen es überhaupt nichts zu tun hat. Wenn eine Simulation ausreichend groß wird, werden sicher ziemlich viele verschiedene Eigenschaften von einem anderen Objekt abgefragt werden müssen. Soll für jede einfach das GameObjekt eine neue virtuelle Funktion bekommen?
    Code wird nicht leserlicher und verständlicher, wenn ein unbeweglicher Geschützturm in irgendeiner Form von Moving abgeleitet wird.



  • GorbGorb schrieb:

    Es läuft am Ende darauf hinaus, dass ein Objekt über Schnittstellen verfügt, mit denen es überhaupt nichts zu tun hat. Wenn eine Simulation ausreichend groß wird, werden sicher ziemlich viele verschiedene Eigenschaften von einem anderen Objekt abgefragt werden müssen. Soll für jede einfach das GameObjekt eine neue virtuelle Funktion bekommen?

    Das kommt darauf an, wie der Raum der Objekte aussieht. Ist er sehr strukturiert, da sheißt, gibt es starke Abhängigkeiten zwischen den Eigenschaften, dann lässt sich das Problem mit mehreren abgeleiteten Klassen problemlos erschlagen.

    Anders sieht es wiederum aus, wenn es wenig Struktur gibt, und die Eigenschaften weitestgehend unabhängig voneinander sind. Dann kann man wirklich in ein Problem der exponentiell vielen Klassen laufen. Meistens ist das aber nicht wirklich der Fall. Statistisch dominieren nur wenige Objekttypen, und der Rest sind Spezialfälle und "Was-wäre-wenns" in der Projektplanung.

    Ich habe deinen Link durchgelesen, bis ich dann nach Seite 3 merkte, dass hinter dem Geschwurbel einfach Data-Oriented Programming steht. Die Idee, voneinander unabhängige Daten getrennt zu speichern und zu verarbeiten ist alt. Und im Gegensatz zu dem Artikel ist das kein Gegensätzliches Konzept zur OOP -> Aggregation. Und es funktioniert auch nur in so wenig Spezialfällen, dass man daraus keine allgemeine Regeln ableiten kann. Die Fälle, in denen die Eigenschaftskombinationen unabhängig voneinander sind, sind wenige. Meistens kann zwar Eigenschaft A ohne Eigenschaft B auftreten, aber wenn beide zusammen auftreten, beeinflussen sie sich gegenseitig. Und schon geht das tolle data oriented programming den Bach runter.



  • cooky451 schrieb:

    @volkard
    Hast du das schon mal mit etwas komplexeren Dingen ausprobiert? Bei mir endet das dann so:

    class Object
    {};
    class MovingObject : virtual public Object
    {};
    class VisibleObject : virtual public Object
    {};
    class PhysicalObject : virtual public Object
    {};
    class ActiveObject : virtual public Object
    {};
    ...
    

    Ach und ein aktives, sichtbares, bewegliches Objekt soll dann wiederum von MovingObject, VisibleObject, ActiveObject abgeleitet werden?

    Viel Spaß bei einer unpflegbaren Klassenhirachie. Vielleicht schon einmal das Strategiemuster angeschaut?

    cooky451 schrieb:

    Es geht natürlich auch so, aber wie verhinderst du dann die dynamic_cast Orgie?

    Auch wenn es nicht unbedingt die Ideallösung ist:

    Object * object = ...;
    if(object->MovingStrategie)
      object->move();
    


  • Ich hab etwas ähnliches wie asc:

    Object * object = ...;
    if (object->is(enum_of_types::MOVABLE))
      static_cast<Movable*>(object)->move();
    

    Hält zwar das Interface sauber, aber zufrieden bin ich damit nicht.



  • asc schrieb:

    Ach und ein aktives, sichtbares, bewegliches Objekt soll dann wiederum von MovingObject, VisibleObject, ActiveObject abgeleitet werden?

    Viel Spaß bei einer unpflegbaren Klassenhirachie. Vielleicht schon einmal das Strategiemuster angeschaut?

    Das war gerade ein negativ Beispiel, aber ist ja toll, dass es so gut ankommt. 😉

    otze schrieb:

    Ich fürchte, dass diese 4 Unterklassen bei mir eine einzige Klassen wären, die die verschiedenen Möglichkeiten einfach durch interne states kodiert. Ich schätze, dass in dem Fall die Klassen, die eine Kombination dieser 4 sind einfach gegenüber den Spezialfällen überwiegen.

    Bedenke dass man hierüber auch Dinge wie Sounds, Trigger, Licht, etc. regeln möchte. Das würde das Interface irgendwann halt schon zumüllen.



  • dynamit-karst schrieb:

    Ich hab etwas ähnliches wie asc:

    Ich sehe da schon gravierende Unterschiede im Detail.
    1. Komposition versus Vererbung.
    2. In deinem Fall ist ein Cast nötig.


Anmelden zum Antworten