Frage an alle die denken, dynamisches casting ist ein Zeichen von schlechtem Softwaredesign


  • Administrator

    @Ishildur,
    Wir haben sogar einen Artikel zum Thema: http://magazin.c-plusplus.net/artikel/Multimethoden

    @mgaeckler,
    Es ist ein Indiz für einen Designfehler. Muss aber nicht zwangsläufig heissen, dass es einer ist. Allerdings gerade bei Anfänger kommt der dynamic_cast viel zu früh zum Einsatz.

    Man kann natürlich auch mehrere Container benutzen: Einen für Kreise, einen für Linien etc und einen der alle Zeichenobjekte enthält. Aber wo da der Vorteil gegenüber einen einfachen, dynamischen cast sein soll.

    Der Vorteil liegt zum einen mal in der Laufzeit. Wenn du mehrere Container verwendest, dann machst du die Unterscheidung zur Kompilezeit. Desweiteren gibt es eine klare Trennung. Wieso etwas zusammenstopfen, wenn es nicht zusammengehört? Nur weil man es kann, ist kein Argument.

    Grüssli



  • Dravere schrieb:

    verwendest, dann machst du die Unterscheidung zur Kompilezeit. Desweiteren gibt es eine klare Trennung. Wieso etwas zusammenstopfen, wenn es nicht zusammengehört? Nur weil man es kann, ist kein Argument.

    Naja, Kreise und Quadrate wollen sicherlich zusammenkommen. Die Reihenfolge im Container bestimmt schon die Z-Order und lauter so praktische Sachen.



  • @Daravere
    Hmm also irgendetwas mache ich falsch:

    #include "Math.h"
    #include "Debug.h"
    
    class IBoundingVolume;
    class BoundingSphere;
    class OrientedBoundingBox;
    
    class IBoundingVolume{
    public:
     static Vector3 CollisionCheck(IBoundingVolume *Volume0,IBoundingVolume *Volume1){
      return Volume0->CollideWith(Volume1);
     }
    
     virtual Vector3 CollideWith(IBoundingVolume *Volume) = 0;
    };
    
    class BoundingSphere:public IBoundingVolume{
    public:
     Vector3 CollideWith(IBoundingVolume *Volume){
      return Volume->CollideWith(this);
     }
    
     Vector3 CollideWith(BoundingSphere *Volume){
      Services::GetInstance()->Acquire<IDebugService>(true)->AddCustomString("BS -> BS");
      return Vector3::Zero;
     }
    };
    

    Dies führt zu einem Stackoverflow, was IMHO bedeutet, dass CollideWith(BoundingSphere*) niemals aufgerufen wird, was IMHO leider auch korrekt ist so. Was mache ich falsch?

    P.S.
    Ich habe deinen zweiten Link auch durchgelesen und tatsächlich mache ich es im Moment mit dem O(1) Doubledispatcher ohen jemals gewusst zu haben, dass dies eine offizielle Technik ist, hatte ich mir gestern nacht ausgedacht. Ich speichere einfach eine ID in der abstrakten IBoundingVolume Klasse und habe ein 2 dimensionales Array von Zeigern auf statische Funktionen, die ach alle in der abstrakten Basisklasse definiert sind. Leider ist das Beast nur sehr schwer erweiterbar, falls irgendwann mal neue Typen hinzukommen. Dies ist aber beim dynamischen double dispatcher IMHO auch nicht anders, immerhin muss ich beim Hinzufügen eines neuen BoundingVolume Types sämtliche bisher existierenden BoundingVolume Typen ebenfalls erweitern...



  • Habs rausgefunden:

    class IBoundingVolume{
    public:
     static Vector3 CollisionCheck(IBoundingVolume *Volume0,IBoundingVolume *Volume1){
      return Volume0->CollideWith(Volume1);
     }
    
     virtual Vector3 CollideWith(IBoundingVolume *Volume) = 0;
     virtual Vector3 CollideWith(BoundingSphere *Volume) = 0;
    };
    

  • Administrator

    volkard schrieb:

    Dravere schrieb:

    verwendest, dann machst du die Unterscheidung zur Kompilezeit. Desweiteren gibt es eine klare Trennung. Wieso etwas zusammenstopfen, wenn es nicht zusammengehört? Nur weil man es kann, ist kein Argument.

    Naja, Kreise und Quadrate wollen sicherlich zusammenkommen. Die Reihenfolge im Container bestimmt schon die Z-Order und lauter so praktische Sachen.

    Wenn man nur die Z-Order bestimmen will oder sie zeichnen muss, dann spricht auch nichts dagegen, die Objekte in den gleichen Container zu stopfen. Dann braucht man aber auch keine Kreise mehr sondern nur noch Graphikobjekte oder sowas.

    Zudem sollte man nicht vergessen, auch wenn man mehrere Container hat, man kann eine Forward-Range über alle Container erstellen. Diese kann sogar nach einem bestimmten Kriterium das nächste Element auswählen.

    @Ishildur,
    Ich habe erst einen Link hier hingeschrieben 🙂
    (Und es heisst nicht Daravere ;))

    Grüssli



  • @Dravere
    sry 😉

    Hmm, wenn ich mir das Ganze ansehe fallen mir zwei negative Dinge auf:
    1. Das Teil ist wie bereits erwähnt sehr schlecht erweiterbar. Jede neue Subklasse erfordert eine Manipulation der Basisklasse sowie sämtlicher bereits existierender Subklassen. (Das ist IMHO nicht gerade schönes OOP und wenn der Source von IBoundingVolume sowie sämtlicher Subklassen nicht offenliegt überhaupt nicht mehr erweiterbar).
    2. Ich muss jede Methode doppelt implementieren, bspw. in der BoundingSphere muss ich eine Kollision für BoundingBox implementieren und in der BoundingBox eine für die Kollistion mit der BoundingSphere, obwohl die Mathematik 1:1 dieselbe ist.

    Da frage ich mich doch, ob meine Version mit statischem O(1) Dispatching nicht schöner ist, denn die kann ich erweitern, ohne die Basisklasse und alle bereits existierenden Subklassen manipulieren zu müssen, dafür habe ich halt 2 (böse) dynamic_casts pro Kollisionstest...



  • mgaeckler schrieb:

    Nagut, vieleicht ist mein Beispiel suboptimal, mir ist jetzt auf die schnelle nichts besseres eingefallen. Ich habe auch nach 20 Jahren C++ Entwicklung dieses Jahr zum ersten mal überhaupt dieses Konstrukt benutzt. Mal schauen ob ich die Anwendung finde, wo ich das benutzt habe.

    mfg Martin

    Jetzt weiß ich wieder wo und warum ich das benutzt habe.

    Ich habe eine C++ Klassenbibliothek zur XML-Verarbeitung. Eine der Anwendungen, die diese Bibliothek benutzt, braucht nun eine Eigenschaft, die diese Bibliothek nicht liefert. Ich wollte es auch der Bibliothek nicht beibringen, weil es nur diese eine Anwendung war und die Bibliothek möglichst effizient sein sollte. Deshalb ermittelt die Anwendung nun mit Hilfe des dynamic casts welche Objektklasse es gerade bearbeitet.

    mfg Martin



  • Ishildur schrieb:

    Hmm, wenn ich mir das Ganze ansehe fallen mir zwei negative Dinge auf:
    1. Das Teil ist wie bereits erwähnt sehr schlecht erweiterbar. Jede neue Subklasse erfordert eine Manipulation der Basisklasse sowie sämtlicher bereits existierender Subklassen. (Das ist IMHO nicht gerade schönes OOP und wenn der Source von IBoundingVolume sowie sämtlicher Subklassen nicht offenliegt überhaupt nicht mehr erweiterbar).

    Das Problem halte ich für überbewertet. Mit einem #if #error oder static_assert noch absichern, daß man keine Klassen vergißt, und ich bin zufrieden.

    Ishildur schrieb:

    2. Ich muss jede Methode doppelt implementieren, bspw. in der BoundingSphere muss ich eine Kollision für BoundingBox implementieren und in der BoundingBox eine für die Kollistion mit der BoundingSphere, obwohl die Mathematik 1:1 dieselbe ist.

    Sehe ich nicht. Die, wo der Klassenname des zweiten Objekts größer ist als der des ersten Objekts, leiten einfach weiter and die echten.

    Ishildur schrieb:

    Da frage ich mich doch, ob meine Version mit statischem O(1) Dispatching nicht schöner ist, denn die kann ich erweitern, ohne die Basisklasse und alle bereits existierenden Subklassen manipulieren zu müssen, dafür habe ich halt 2 dynamic_casts pro Kollisionstest...

    Ich sehe einfach keine dynamic_casts. Dein Kern war doch die Funktionszeigertabelle.

    class IBase{
      enum RealType{KREIS,QUADRAT,ZWICKEL,DREIECK,EI,ANZAHL,};
      virtual RealType getRealType()=0;
    ...
    bool table[ANZAHL][ANZAHL](IBase*,IBase*)={...
    ...
    if(table[item1->getRealType()][item2->getRealType()](item1,item2))...
    


  • mgaeckler schrieb:

    nun eine Eigenschaft, die diese Bibliothek nicht liefert.

    Also Designfehler in der Bibliothek. Und der dynamic_cast hat ihn aufgedeckt.



  • Double Dispatching/Multimethoden sind ja nur der Anfang des Kapitels aus dem Buch. Im weiteren Verlauf beschreibt Meyer eine Tabelle aus Funktionszeigern, die manuell aufgebaut werden muss. Ironischerweise wird bei der Implementation wieder ein dynamic_cast verwendet, den wir eigentlich loswerden wollten.
    Meiner Meinung nach sollte man Meyer nicht blindlings vertrauen (zumindest was seinen Vorschlag angeht), sondern sich vernünftige Gedanken über seinen Anwendungsfall machen. Wenn abzusehen ist, dass man nie mehr als zwei oder drei verschiedene Typen vergleichen muss lohnt sich das double-dispatching in meinen Augen nicht. Wenn das Design jedoch flexibel und erweiterbar sein soll sollte man sich mit double-dispatching/Multimethoden oder Funktionstabellen auseinandersetzen. Meyer schliesst das Kapitel mit der Feststellung ab, dass es keine einfach Lösung für dieses Problem gibt und man eine Lösung wählen sollte, die am ehesten dem Bedarf entspricht.



  • volkard schrieb:

    mgaeckler schrieb:

    nun eine Eigenschaft, die diese Bibliothek nicht liefert.

    Also Designfehler in der Bibliothek. Und der dynamic_cast hat ihn aufgedeckt.

    Mag sein. Der Terminus Fehler bezeichnet für mich eine Eigenart, die geändert werden muß. Nun ist es aber so, daß es häufig vorkommt, daß bestimmte Anforderungen sich gegenseitig ausschließen.

    Beispiel: Der Einsatz virtueller Funktionen steigert sicherlich die Wartbarkeit und Wiederverwendbarkeit einer Implementation veringert dafür aber die Effizienz des Programmes. Nun gilt es eben einen Kompromiss zu finden, zwischen diesen beiden Anforderungen.

    Es gilt daher wie so oft, wenn eine bestimmte Technik eingesetzt werden soll, muß man/frau auch immer deren Nachteile kennen und dann von Fall zu Fall entscheiden, was sinnvoll ist.

    mfg Martin



  • Wenn dynamic_cast klappt, hast Du eh die vtbl im code und den vptr in jedem Objekt. Da wird durch eine weitere virtuelle Funktion nichts langsamer.



  • mgaeckler schrieb:

    Mag sein. Der Terminus Fehler bezeichnet für mich eine Eigenart, die geändert werden muß.

    Dann ist der Terminus bei Dir falsch belegt, fürchte ich.
    Nehmen wir uns mal das Posting von DocShoe. Er schreibt da von einem Herrn Meyer, auf den man nicht so hören soll. Eindeutig ein Fehler. Der gemeinte Mann heißt Meyers. Trotzdem besteht absolut kein Bedarf, das zu ändern.



  • volkard schrieb:

    Dann ist der Terminus bei Dir falsch belegt, fürchte ich.
    Nehmen wir uns mal das Posting von DocShoe. Er schreibt da von einem Herrn Meyer, auf den man nicht so hören soll. Eindeutig ein Fehler. Der gemeinte Mann heißt Meyers. Trotzdem besteht absolut kein Bedarf, das zu ändern.

    Vielleicht solltest du dir das Posting mal richtig durchlesen, statt nur auf Details wie Namen zu achten. Ich habe nicht behauptet, immer auf Meyers Techniken zu verzichten, sondern zu überlegen, ob sie für den eigenen Anwendungsfall nützlich und anwendbar sind. Klar kann ich mehrere zig Zeilen Code schreiben, der eine Designschwäche behebt, aber den Code aufbläht und dafür andere Nachteile mit sich bringt. Das stoische Anwenden von Schema F auf Problem P ist nicht immer die beste Lösung.



  • DocShoe schrieb:

    volkard schrieb:

    Dann ist der Terminus bei Dir falsch belegt, fürchte ich.
    Nehmen wir uns mal das Posting von DocShoe. Er schreibt da von einem Herrn Meyer, auf den man nicht so hören soll. Eindeutig ein Fehler. Der gemeinte Mann heißt Meyers. Trotzdem besteht absolut kein Bedarf, das zu ändern.

    Vielleicht solltest du dir das Posting mal richtig durchlesen, statt nur auf Details wie Namen zu achten. Ich habe nicht behauptet, immer auf Meyers Techniken zu verzichten, sondern zu überlegen, ob sie für den eigenen Anwendungsfall nützlich und anwendbar sind. Klar kann ich mehrere zig Zeilen Code schreiben, der eine Designschwäche behebt, aber den Code aufbläht und dafür andere Nachteile mit sich bringt. Das stoische Anwenden von Schema F auf Problem P ist nicht immer die beste Lösung.

    Ich fühle mich rundherum mißverstanden.



  • Gnihihi, dein Beitrag lässt aber auch genug Spielraum für Fehlinterpretationen. Du solltest klarstellen, worin der Fehler liegt, um weiteren Missverständnissen vorzubeugen.



  • volkard schrieb:

    Wenn dynamic_cast klappt, hast Du eh die vtbl im code und den vptr in jedem Objekt. Da wird durch eine weitere virtuelle Funktion nichts langsamer.

    Doch. Die Programme, die diese Bibliothek benutzen werden größer und zwar alle unabhängig davon, ob sie die Funktionen alle nutzen oder nicht. Das macht die Programme daher zwangsweise langsamer. Ob sich das bemerkbar macht oder nicht, hängt natürlich vom Einzelfall ab. In einem Fall ging es um eine Lasttestanwendung, die möglichst viel Last auf einem Server generieren sollte ohne aber selbst unnötig Overhead zu erzeugen.

    mfg Martin



  • DocShoe schrieb:

    Gnihihi, dein Beitrag lässt aber auch genug Spielraum für Fehlinterpretationen. Du solltest klarstellen, worin der Fehler liegt, um weiteren Missverständnissen vorzubeugen.

    Der Fehler ist das vergessene s im Namen. Das ist einerseits eindeutig ein Fehler und andererseits völlig unwichtig und muß nicht repariert werden. Es war ein naheliegendes Beispiel, um den Terminus "Fehler" ein wenig zu rücken.



  • volkard schrieb:

    DocShoe schrieb:

    Gnihihi, dein Beitrag lässt aber auch genug Spielraum für Fehlinterpretationen. Du solltest klarstellen, worin der Fehler liegt, um weiteren Missverständnissen vorzubeugen.

    Der Fehler ist das vergessene s im Namen. Das ist einerseits eindeutig ein Fehler und andererseits völlig unwichtig und muß nicht repariert werden. Es war ein naheliegendes Beispiel, um den Terminus "Fehler" ein wenig zu rücken.

    Wurde mir dann auch klar... du hättest dich aber auch auf "auf den man nicht hören soll" beziehen können, was deinem Beitrag einen ganz anderen Sinn gegeben hätte.



  • mgaeckler schrieb:

    Doch. Die Programme, die diese Bibliothek benutzen werden größer

    Was wird grösser? Der zusätzliche VTable-Eintrag?

    mgaeckler schrieb:

    Das macht die Programme daher zwangsweise langsamer.

    Aber daran, dass dynamic_cast in vielen Implementierungen sehr langsam ist, denkst du nicht.

    mgaeckler schrieb:

    Ob sich das bemerkbar macht oder nicht, hängt natürlich vom Einzelfall ab. In einem Fall ging es um eine Lasttestanwendung, die möglichst viel Last auf einem Server generieren sollte ohne aber selbst unnötig Overhead zu erzeugen.

    Ja, und genau deshalb solltest du messen. Erst recht, wenn es um eine zeitkritische Anwendung geht. Bitte meide nicht virtuelle Funktionen und weiche auf fragwürdige Alternativen wie dynamic_cast aus, weil du das Gefühl hast, virtuelle Funktionen seien langsam.


Anmelden zum Antworten