Programmdesign eines Interpreters
-
Lars Hupel schrieb:
Datentyp |- Skalar |- natürliche Zahl |- ganze Zahl |- rationale Zahl etc. |- Geometrie etc.Das könnte besser klappen. Dann könnte man Operationen wie folgt definieren:
Exact operator + (const Exact& first,const Fraction& second); Fraction operator - (const Fraction& first,int second); // etc.camper schrieb:
in diesem konkreten fall habe ich allerdings sowieso zweifel, dass eine klassenhierarchie überhaupt zweckmäßig ist. für binäre operation bräuchte man doch dem grunde nach eine double-dispatch methode, sofern nicht immer beide operanden vom selben typ sein sollen. virtuelle funktionen helfen hier nicht unbedingt weiter und double-dispatch lässt sich auch gut ohne vererbung implementieren.
Worum handelt es sich bei double-dispatch und LSP?
-
LSP = Liskovsches Substitutionsprinzip, einfach mal googlen
virtuelle funktionen sind eine form von single-dispatch: der konkrete funktion, die aufgerufen wird, hängt vom dynamischen typ eines parameters (des impliziten objekt parameters) ab. dem gegenüber wählt double-dispatch die aufzurufende funktion anhand des dynamischen typs zweier parameter aus (es macht einen unterschied, ob ich zwei integer, ein integer und eine rationale zahl oder zwei rationale zahlen addiere) - die brutalstmöglich einfache methode, das zu erreichen, sind dynamic_cast orgien...
wenn ich das problem richtig verstehe, ist der 'zahlentyp' in einem solchen intepreter grundsätzlich eine dynamische angelegenheit. man könnte sich auch leicht operationen vorstellen, die den 'typ' einer variablen ändern. möglicherweise würde ein single-element container (boost::any kommt in den sinn) Scalar, der dann jeweils zu einem zeitpunkt einen wert eines zahlentyps enthält, dienlich sein. selbstverständlich könnten dann immer noch alle zahlentyp von etwas abstrakteren abgeleitet sein - das wäre dann aber eher ein implementationsdetail, und nicht teil des interfaces von Scalar.
-
Sorry dass ich erst jetzt schreibe. Habe nicht immer Internet verfügbar.
Also LSP weiß ich jetzt was das ist.
Also ehrlich gesagt habe ich keine Lust auf dynamic_cast, was ja wohl auch verständlich ist. Ich finde solchen Code nicht so elegant

Jetzt ist das Problem, wie eine elegante Lösung aussieht. Um mein Problem noch einmal klar zu umreißen:
- Einmal im Skript deklariert, soll der Datentyp einer Variable fest bleiben. Jedoch soll der Datentyp automatisch erkannt werden bzw. ggf. explizit angegeben werden.
- Eine Basisklasse ist wichtig, da ich vorhabe, eine Symboltabelle à la "std::mapstd::string,Scalar* zu erstellen. Mehrere Symboltabellen wären zwar einfacher, aber durchaus lästig und unelegant.
Ganz wichtig: Die einzelnen Typen haben verschiedene Wertigkeiten. Das heißt, eine reelle Zahl z. B. 1, rationale Zahl 2, ganze Zahl 3 etc. Wenn eine Operation auf Operanden angewandt wird, so soll das Ergebnis die Wertigkeit des jenigen Operanden übernehmen, der die kleinere Wertigkeitszahl hat. Bsp: - Ich gebe zu, ich bin faul, also möchte ich mir gerne explizite Casts und enums (so was wie SCALAR_R, SCALAR_Q, SCALAR_N etc.) ersparen.
Wenn ich jetzt meinen Ansatz mal auf folgendes Minimalbeispiel reduziere:
class Scalar { public: virtual Scalar& add(const Scalar& other); }; class Fraction : public Scalar { public: virtual Fraction& add(const Fraction& other); virtual Fraction& add(const UInteger& other); virtual Fraction& add(const Integer& other); };Wie kriege ich (elegant
) den Compiler dazu, dass er das so kompiliert, wie ich will
Also ich meine, dass bei folgendem SnippetFraction f; Scalar& s = f; s.add(f);Fraction::add und nicht Scalar::add aufgerufen wird. Wie geht das? Muss ich vielleicht meinen Ansatz komplett umstrukturieren

-
push
-
Oja, das selbe Problem hab ich auch... Hat den keiner eine Lösung?
Danke
-
Ich hab das ganze jetzt mal so verändert.
class Scalar { public: virtual Scalar& add(const Scalar& other) = 0; }; class Fraction : public Scalar { public: virtual Fraction& add(const Scalar& other) { if (typeid(other)==typeid(Fraction)) this->add((Fraction&)other); if (typeid(other)==typeid(UInteger)) this->add((UInteger&)other); if (typeid(other)==typeid(Integer)) this->add((Integer&) other); } virtual Fraction& add(const Fraction& other); virtual Fraction& add(const UInteger& other); virtual Fraction& add(const Integer& other); };Für wie elegant haltet ihr das?
Was ich allerdings noch nicht verstehe, ist folgendes:
class Scalar { public: virtual Scalar& add(const Scalar& other) = 0; }; class Fraction : public Scalar { public: virtual Fraction& add(const Scalar& other) { this->add(other); } virtual Fraction& add(const Fraction& other); virtual Fraction& add(const UInteger& other); virtual Fraction& add(const Integer& other); };Eigentlich müsste die vtable zur Laufzeit doch erkennen, dass z. B. add(const Fraction&) aufzurufen ist. Allerdings kommt es zu einer Endlosrekursion.

-
Lars Hupel schrieb:
Für wie elegant haltet ihr das?
nö, nicht so. dynamic_cast und typeid sind nur für notfälle da.
das einzige, was du machen musst, ist vorher mit virtuellen funktonen die richtigen statischen funktionen rauszufinden. siehe buch "mehr effektiv c++ programmieren". das ist der code, den du nicht verstanden hattest.
//stufe 0 Scalar* add(const Scalar& a,const Scalar& b) { //leider mit new anlegen, weil du den typ des ergebnisses //ja noch gar nicht kennen tust //oder hab ich nen trick übersehen, wie du dafür sorgst, daß alle typen //dieser hiearchie gleich groß sind und mutieren können? //beide typen unbekannt. ich mache einen belannt. //durch den virtuel-trick wird der typ von a bekannt gemacht. return a->add(b); } class Scalar { public: //stufe 1 virtual Scalar* add(const Scalar& b)=0; //stufe 2 virtual Scalar* add(const Fraction& b)=0; virtual Scalar* add(const UInteger& b)=0; virtual Scalar* add(const Integer& b)=0; }; class Fraction : public Scalar { public: //stufe 1 Scalar* add(const Scalar& b) { //aha, der typ von a ist bekannt. nur mach ich den von b mit dem //gleichen trick auch bekannt b->add(*this);//trick 17. beachte, daß der genaue typ von this bekannt ist } //stufe 2 Fraction* add(const Fraction& b); Fraction* add(const UInteger& b); Fraction* add(const Integer& b); };sind bestimmt noch fehler drin, hoffe, es klappt trotzdem bei dir.
-
Hallo, danke erstmal. Aber irgendwie verstehe ich deinen Code noch nicht so ganz. Was legst du mit new an? Außerdem funktioniert das nicht so ganz was du da machst. Schließlich heißt es Fraction::add(const Fraction&) und nicht Fraction::add(const Fraction&) const. Das heißt wiederum, dass du für einen const Fraction& nicht add aufrufen kannst, weil diese Funktion nicht konstant ist.
(Möglicherweise haben wir uns missverstanden. add() soll Ersatz für operator += () sein, also die Variable verändern. Ich hab mir das so angewöhnt. Aber vielleicht macht es ja weniger Ärger, wenn es den operator + () ersetzt.)
-
vllt gehts ja besser wenn add als operator + agiert? dann kannst du es als const machen
-
Hallo,
ich hab jetzt mal versucht das ganze zu implementieren. Das funktioniert aber nicht so toll - es gibt Compilerfehler.
Meine "Interpretation":
#include <iostream> class Fraction; class Scalar { public: //stufe 1 virtual Scalar& add(const Scalar& b)=0; //stufe 2 virtual Scalar& add(const Fraction& b)=0; }; //stufe 0 Scalar& add(const Scalar& a,const Scalar& b) { return a.add(b); } class Fraction : public Scalar { public: //stufe 1 Scalar& add(const Scalar& b) { b.add(*this); } //stufe 2 Fraction& add(const Fraction& b) { //code } }; int main() { Fraction f; }Die Fehler:
C:\Dokumente und Einstellungen\user\Desktop\Unbenannt1.cpp: In function `Scalar& add(const Scalar&, const Scalar&)': C:\Dokumente und Einstellungen\user\Desktop\Unbenannt1.cpp:17: error: passing `const Scalar' as `this' argument of `virtual Scalar& Scalar::add(const Scalar&)' discards qualifiers C:\Dokumente und Einstellungen\user\Desktop\Unbenannt1.cpp: In member function `virtual Scalar& Fraction::add(const Scalar&)': C:\Dokumente und Einstellungen\user\Desktop\Unbenannt1.cpp:26: error: passing `const Scalar' as `this' argument of `virtual Scalar& Scalar::add(const Fraction&)' discards qualifiersIch glaube da fehlen noch ein paar Codestücke in meiner Implementation, richtig? Was meinst du außerdem mit Stufe 0 etc. Und welchen Code hatte ich nicht verstanden? Außerdem brauche ich doch keine statischen Funktionen.
Könntest du mir deinen Code bitte etwas näher erläutern?
-
Da steht doch der Fehler genau da - du hast ein const Scalar und willst darauf eine nicht-konstante Methode anwenden. Also entweder du deklarierst die Add()-Methode als konstant (dann darf sie aber nicht mehr ihr this-Objekt ändern) oder du erzeugst dir eine temporäre Variable, auf die du addieren kannst:
Scalar add(const Scalar& a,const Scalar& b) { Scalar tmp=a; tmp.add(b); return tmp; }
-
Das funktioniert so auch nicht - Scalar ist rein abstrakt.
Aber ich habe jetzt einen anderen Ansatz: es gibt intern einen Datentyp und wenn nötig wird dieser umgewandelt. Der Benutzer bekommt davon also nichts mit (ist vielleicht zweckmäßiger).