Frage an alle die denken, dynamisches casting ist ein Zeichen von schlechtem Softwaredesign
-
Ist schon krass, was dieses Forum für einen Mehrwert bringt! Ich wäre vielleicht früher oder später selbst darauf gestossen aber dann hätte ich zwei, drei oder mehr Stunden im Inet rumgewurstelt ohne wirlich zu wissen wonach ich suche. Ohne das richtige Schlagwort zu kennen hat man IMHO fast keine chance

Double dispatch is useful in situations where the choice of computation depends on the runtime types of its arguments. For example, a programmer could use double dispatch in the following situations:
Adaptive collision algorithms usually require that collisions between different objects are handled in different ways. A typical example is in a game environment where the collision between a spaceship and an asteroid will be computed differently than the collision between a spaceship and a spacestationMan könnte fast den Eindruck bekommen, dass ich nicht der erste wäre, der auf dieses Problem stösst :p
-
Hallo,
die Aussage, dynamisches casten sei ein Zeichen schlechten Softwaredesigns, ist völliger Humbug. Natürlich kann man auch ohne auskommen, dann muß man aber letztendelich explizit das (oder was ähnliches) implementieren, was der Compiler sonst implizit täte.
Ein einfaches Beispiel:
Ein Container enthält verschiedene Zeichenobjekte: Kreise, Rechtecke, Punkte, Linien etc.Nun soll zu einem Objekt der Radius ermittelt werden. Der Container muß feststellen können, ob das Objekt überhaupt ein Kreis ist, da Linien gar keinen Radius haben. Das geht mit dynamischen casten am einfachsten.
KREIS *circle = dynamic_cast<KREIS *>item; if( circle ) return circle->radius(); else return -1;Natürlich ginge auch sowas:
return ((KREIS*)item)->radius();Ich denke aber, daß es hier einen Konsens gibt, daß dies völliger Unfug ist. Aber auch das ist nicht viel besser:
if( item->isCircle() ) // wie auch immer das gelöst sein mag return ((KREIS*)item)->radius(); else return -1;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.
Die Aussage sieht mir eher danach aus: Was ich nicht brauche, kann nur Unfug sein.
mfg Martin
-
ogni42 schrieb:
Volkard hat es ja schon erwähnt.
Die wikipedia liefert Dir auch ein Codebeispiel für Kollision:
http://en.wikipedia.org/wiki/Double_dispatchDas ist ja genial. Da haben wir doch den Krug des wertvollen Wissens wieder etwas mehr befüllt!

-
mgaeckler schrieb:
die Aussage, dynamisches casten sei ein Zeichen schlechten Softwaredesigns, ist völliger Humbug.
Nein! Sie ist nur schwacher Humbug.
mgaeckler schrieb:
Natürlich kann man auch ohne auskommen, dann muß man aber letztendelich explizit das (oder was ähnliches) implementieren, was der Compiler sonst implizit täte.
Hier ist die Humbug-Quote schon größer.
KREIS *circle = dynamic_cast<KREIS *>item; if( circle ) return circle->radius(); else return -1;Wenn Du den Radius überlädst mit einem Spezialwert mit der Bedeutung ICHBINKEINRUNDESDING und behauptest, daß
Rechteck::radius(){return ICHBINKEINRUNDESDING;}, dann schreib doch einfach
Rechteck::radius(){return ICHBINKEINRUNDESDING;}hin.
Auf jeden Fall gehört die Entscheidung, ob ein Ding rund ist nicht in den Anwendercode mit dynamic_cast, sondern in die Klassen rein.
if( obj.hatRadius() ) ...Die Überladung des Return-Wertes in Deinem Anwendercode ist nicht gerade unbedenklich. Aber wenn Du schon so einen Krempel machst, dann warum nicht durchziehen bis unten hin?
-
volkard schrieb:
mgaeckler schrieb:
die Aussage, dynamisches casten sei ein Zeichen schlechten Softwaredesigns, ist völliger Humbug.
Nein! Sie ist nur schwacher Humbug.
mgaeckler schrieb:
Natürlich kann man auch ohne auskommen, dann muß man aber letztendelich explizit das (oder was ähnliches) implementieren, was der Compiler sonst implizit täte.
Hier ist die Humbug-Quote schon größer.
...
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
-
@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 derdynamic_castviel 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; };
-
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.