Interpreter für eine kleine Programmiersprache
-
Warum benutzt du überhaupt Zeiger? Auch bei den anderen Funktionen solltest du direkt die Werte (bzw.
vector<...>
) zurückgeben.In Headerdateien sollten nur Deklarationen, die eigentlichen Funktionsdefinitionen dann in dazu passende Source-Dateien (*.cpp).
Und m.E. wäre es besser, du würdest Klassen erzeugen und dort dann die Funktionen deklarieren/definieren (das würde z.B. die Übergabe der Parameter vereinfachen, wenn in
Lector.h
der Parameterconst vector<string>& line
als Member definiert wäre).Aber auch inhaltlich sind deine bisherigen Funktionen noch optimierbar - du hast sehr viel doppelten (und damit redundanten) Code.
DeineprintTree
-Funktion z.B. könnte man durch Verwendung eines Arrays (für die Namen der Befehle) stark reduzieren (und nur die Ausnahmen explizit ausprogrammieren).
-
@Th69 Zeiger benutze ich vor allem deshalb, da ich eine relativ hohe Performanz erreichen möchte und somit - so mein Gedanke - kein Kopieren der einzelnen Nodes beim Iterieren stattfinden muss. Wie würdest du das ansonsten machen? Ich habe es ohne Zeiger versucht und der Interpreter war um ein Vielfaches langsamer. Das mit den Source-Dateien ist ein guter Vorschlag. Das setze ich so um. Danke Dir.
-
@patds20 sagte in Interpreter für eine kleine Programmiersprache:
... kein Kopieren der einzelnen Nodes beim Iterieren stattfinden muss.
Welche Funktion meinst du?
parseTree
?
Ich meinte u.a. diemake/getXEntry
-Funktionen inParser.h
.
-
@patds20 sagte in Interpreter für eine kleine Programmiersprache:
@Mechanics Was wäre dir an der Stelle lieber?
std::unique_ptr<long double> newVar = std::make_unique<long double>();
So etwas? Oder soll ich new verwenden?
Das kommt drauf an, wo/wie das verwendet wird. Ich habe mir nicht den ganzen Code angeschaut. Aber mir war auf den ersten Blick überhaupt nicht klar, wer die ganzen Zeiger wieder löscht und ich gehe davon aus, dass da Memory Leaks sind. Rohe besitzende Zeiger sollte man in C++ grundsätzlich nicht benutzen. Aber das malloc war schon lustig.
-
Die klassichen
{}
durch[]
für Kontrollblöcke zu ersetzen ist ein Geniestreich.Spaß beiseite, man sieht, dass das Projekt mit Liebe gemacht wurde, muss man loben.
Ich will schon lange als nächsten Schritt zum einfachen Taschenrechner einen Lisp-Interpreter schreiben aber bin nicht mal mit der Grammatik fertig geworden lach.Mich würde interessieren, ob Du auch eine BNF o. Ä. Grammatik spezifiziert hast für die Sprache?
Schonmal an Unterstützung für komplexe Zahlen gedacht?
Was machtrepairTokens
(Beispiel)?
Was auch ziemlich elegant wäre, ist eine SourceCodePos oder ähnliches im Lexer zu führen und dann bei Fehlermeldungen genau Zeile/Stelle auszugeben. Das habe ich selbst noch nie implementiert, aber so im Hinterkopf als nice-to-have Feature.Ansonsten wurde ja schon gesagt
using namespace std;
in Header-Dateien ist keine gute Angewohnheit.Statt
void*
tut's evtl. std::any oder std::variant, oder halt so mehrere Typen im Struct speichern und je nach Kennung zugreifen, eine Art variant-Eigenbau.calculateExpression
wäre evtl. mit einemswitch
sauberer gelöst, außer ich übersehe etwas offensichtliches.Man muss sich dann auf jeden Fall folgenden Editor besorgen, um in der Sprache zu programmieren: https://brackets.io/ :o)
Und zuletzt: Ich war gerade total verwirrt, dass das Projekt nur aus Header-Dateien besteht --> Keine Lust auf Sourcefiles?
So viel zum ersten spontanen Überfliegen. Glückwunsch, das Projekt bisher so weit gebracht zu haben
-
Ich persönlich liebe eckige Klammern und scheitere immer fürchterlich daran, schöne geschweifte Klammern zu zeichnen. Daher wollte ich der Sprache einen kleinen persönlichen Touch verpassen.
@HarteWare Eine BNF habe ich nicht dafür erstellt, da das mit der Mnemonic-ähnlichen Struktur m.E. wenig Sinn ergeben hätte, da Funktionen etc. beispielsweise nicht verschachtelt auftreten können. Jedes Kommando hat ja eine fixe Anzahl an Parametern. Da wäre die Grammatik relativ langweilig bis auf die MathNode-Struktur.
Das mit den komplexen Zahlen klingt spannend. Danke für die coole Idee.
repairTokens repariert die Tokens, wenn z.B. kein Leerzeichen nach dem "do" bei Schleifen gelassen wird und solche kleinen Dinge. Das mit der SourceCodePos wollte ich auch mal machen, hatte das dann aber wieder vergessen. Das ist natürlich viel einfacher als immer die Zeilen ohne Kommentare und Leerzeilen im Kopf zu zählen.
Ich werde die gesamten Dateien dann die Tage einmal neu formatieren / umsetzen und die Header-Files ausmisten. Ich hatte die eigentlich angelegt, um die includes ein wenig einfacher zu machen. Aber mit Source Files ergibt das deutlich mehr Sinn.
Vielen Dank für deine tollen Ratschläge.
-
@patds20: Ich habe gerade gesehen, daß du bei deinem Projekt einfach nur die Header in
*.cpp
umbenannt hast (und diese dann per#include
einbindest) - so ist das nicht gedacht!Die Quelldateien sollten getrennt übersetzt werden, am besten mittels eines Projekts oder einem Makefile, und es werden dann nur die Headerdateien mit den Deklarationen per
#include
eingebunden.
Welche IDE bzw. Compiler-Umgebung benutzt du denn?Schau auch mal in Wann Header-Datei und wann Cpp-Datei sowie Headerdateien (C++) bzw. in englisch Introduction to the compiler, linker and libraries.
-
@Th69 Das ist auch noch keine finale Lösung. Ich wollte aber schon einmal alle Dateien umbenennen und mich dann heute oder die Tage damit enger befassen. Ich muss die ganzen includes ja dann einmal neu machen etc. Als Umgebung benutzte ich C-Lion. Soll ich die Tokens als Header-File belassen? Dort gibt es ja primär nur Definitionen.
-
Hallo @patds20 ,
Mir hat man es mal so erklärt:- Unterscheide Deklaration und Definition:
1.1 Deklaration: Bekannt machen von Variablen, Typen und Funktionen (auch Methoden von Klassen)
1.2 Definition: Ausgestaltung von Variablen, Typen und Funktionen (auch Methoden von Klassen) - Deklarationen kommen i.d.R. in sogenannte Header-Dateien (*.h), diese werden mittels #include eingebunden.
- Definitionen kommen i.d.R. in CPP-Dateien (*.cpp), diese werden entweder mit kompiliert oder dass Kompilat wird als LIB oder DLL eingebunden.
- Ausnahmen bestätigen die Regel, z.B. Template-Klassen.
Nun einige Worte zu Deinem Code; warum benutzt Du keine Klassen, wie schon von @Th69 angemerkt? Warum nutzt Du überhaupt C++?
Ich würde empfehlen, Dich etwas mit der objektorientierten Programmierung zu beschäftigen; dafür wurde C++ entwickelt.
Und dann kannst Du Dir überlegen, warum Du so exzessiv "inline" nutzt. Wegen der MS Aussage "Die Verwendung von Inlinefunktionen kann das Programm schneller machen, da so der Mehraufwand vermieden wird, der Funktionsaufrufen zugeordnet ist. Der Compiler kann inline erweiterte Funktionen auf Arten optimieren, die für normale Funktionen nicht verfügbar sind."?
Ich habe das Gefühl, dass Du nicht mit C++ vertraut bist, ggf. mit C. Ich würde mir entweder die richtige Programmiersprache für mein Projekt aussuchen oder die genutzte Programmiersprache versuchen zu beherrschen.
Ansonsten; viel Glück und Spaß mit Deinem Projekt.
- Unterscheide Deklaration und Definition:
-
Hab mir das jetzt auch noch mal angeguckt:
-
ich benutze für den std-namespace den vollqualifizierten Namen, ohne den std-Namespace zu öffnen. Die fünf Zeichen mehr machen beim Schreiben den Braten nicht fett und man erkennt sofort, dass es etwas aus der STL ist.
-
übergib' notwendige Parameter nicht als Zeiger, sondern als Referenz. Ich halte mich dabei an folgende Faustregel:
- der Parameter ist optional: Übergabe als (const) Zeiger
- der Parameter ist notwendig und kann verändert werden: Übergabe als Referenz
- der Parameter ist notwendig und darf nicht verändert werden: Übergabe als const-Referenz
-
benutz' so viel aus der STL wie möglich und vermeide handgeschriebene Schleifen. Die C++ Lösung deiner
removeSpaces
Funktion könnte so aussehen:
#include <string> #include <algorithm> #include <cctype> #include <iostream> std::string RemoveSpaces( std::string const& str ) { std::string retval; auto predicate = []( char ch ) { return !std::isspace( ch ); }; std::copy_if( str.begin(), str.end(), std::back_inserter( retval ), predicate ); return retval; }
oder in-situ auf der Variable selbst
#include <string> #include <algorithm> std::string str = "abc def"; str.erase( std::remove( str.begin(), str.end(), ' ' ), str.end() );
- warum legst du deine Variablen überhaupt per
new
an? Wenn du sie in einemstd::unordered_set
hältst werden deren Adresse auch nicht ungültig (es sei denn, du löschst das Element aus dem set), du kannst also die Variable als value im set halten und trotzdem mit deren Adressen arbeiten. Habe mir jetzt aber auch nicht so genau angeguckt, was du mit den Adressen so alles anstellst. - vermeide besitzende Rohzeiger und benutz' stattdessen die smart pointer der STL.
Edit:
- Generell arbeitest du zu wenig mit Rückgabewerten aus Funktionen. Die freie Variable
error
ließe sich komplette entfernen, wenn die Funktion, die sie setzen, den Status statt void zurückgeben. Damit weiß der Aufrufer direkt, ob der Aufruf erfolgreich war oder nicht. exit()
ist in den allerwenigsten Fällen sinnvoll. Stell dir vor, du stellst deine Engine als Bibliothek zur Verfügung und unter bestimmten Umständen wirdexit()
aufgerufen und das Programm beendet. Das ist kann für böse Überraschungen sorgen, wenn die Scripting-Engine ein kleiner und unwichtiger Teil eines Programms ist, aber plötzlich das Programm beendet.
-