Implementation eines Konfigurationsparsers mit Hilfe von boost::spirit (Teil 4)
In den bisherigen drei Teilen dieser Serie haben wir ein Format fuer Konfigurationsdateien erarbeitet, eine Spirit-Grammatik dafuer geschrieben und zuletzt eine Objekthierarchie fuer die Speicherung der Daten implementiert. Der wichtigste Teil fehlt jedoch noch: Die Erzeugung des Objektbaums waehrend des Parsens. Wir werden zu diesem Zweck direkt semantische Aktionen mit den einzelnen Regeln der Grammatik assoziieren.
Warum benutzen wir nicht einfach den von Spirit automatisch generierten Baum? Wir koennten den Parser bereits im jetzigen Zustand (siehe Ende des Teils 2b) benutzen und den von Spirit generierten Objektbaum im aeusseren Attribut verwenden, um nachtraeglich den von uns gewuenschten Baum aus element Objekten aufzubauen. Betrachten wir jedoch einmal beispielsweise den Typ des automatisch generierten Attributs der Regel array, die wie folgt definiert war:
arrayDefinition = lit('[') >> -( double_ % ',' | long_ % ',' | bool_ % ',' | string % ',' ) > ']' ;
Diese Regel generiert ein Attribut vom Typ
optional< variant< vector<double>, vector<long>, vector<bool>, vector<string> > >
Die array Regel wird in listDefinition und variableAssignment verwendet, die ebenfalls ein variant generieren, welches wiederum das oben gezeigte variant der arrayDefinition in seiner Typenliste enthaelt usw. Wir wuerden fuer die aeussere Regel also ein Attribut erhalten, dessen Typdefinition so lang und verschachtelt ist, dass niemand ernsthaft darueber nachdenken wuerde, damit ueberhaupt eine Auswertung zu versuchen. Wir muessen also eine Loesung finden, uns von der statischen Typisierung zu loesen. Unsere element Klassenhierarchie ist dazu bestens geeignet und soll daher anstelle des automatisch generierten Baums verwendet werden.
Spirit ist eine DSEL (Domain Specific Embedded Language), die auf dem Sprachraum von C++ definiert ist. Dies bringt einige Nachteile mit sich, die insbesondere bei der Implementierung der semantischen Aktionen zum Tragen kommen. Da die Grammatik mitsamt der assoziierten semantischen Aktionen vom Compiler in ein Objekt uebersetzt wird, koennen wir fuer diese nicht einfach gewoehnlichen C++ Code benutzen, sondern muessen diesen wiederum in Objekte (Funktoren) kapseln, die dann zur Laufzeit ausgewertet werden koennen. Gluecklicherweise bietet boost::phoenix bereits eine Reihe von Funktoren an, die die meisten grundlegenden Operationen abdeckt (new, delete, Konstruktorenaufrufe, …).
Beginnen wir mit der atom Regel. Wir fuegen dieser eine semantische Aktion hinzu, die mit dem gesamten Ausdruck assoziiert sein soll (dazu klammern wir die Unterregeln einfach ein):
atom = ( double_ | long_ | bool_ | string ) [ _val = new_<cfg::atom>(_1) ] ;
Semantische Aktionen werden in eckige Klammern eingeschlossen und der Teilregel angehaengt, fuer die das Attribut verarbeitet werden soll. Der Typ des generierten Attributs fuer den gesamten Ausdruck
( double_ | long_ | bool_ | string )
ist
variant<double, long, bool, string>
Wir erinnern uns, dass wir in unserer Atom Klasse einen Konstruktor mit genau diesem Typ als Parameter definiert hatten. Normalerweise koennten wir dann ein Objekt vom Typ atom erzeugen, indem wir
new atom(generated_attribute)
schreiben. Fuer Spirit muessen wir dies allerdings in einen phoenix Ausdruck uebersetzen (new_ ist der phoenix Funktor fuer new).
Auf das generierte Attribut koennen wir ueber den Platzhalter _1 zugreifen. Den Zeiger auf das neu erzeugte atom Objekt weisen wir dem Platzhalter _val zu, der diesen als das generierte Attribut der gesamten Regel festlegt. Fassen wir noch einmal zusammen: Ohne die semantische Aktion wuerde die atom Regel ein Attribut vom Typ variant zurueckliefern, jedoch moechten wir stattdessen ein atom Objekt aus unserer Objekthierarchie zurueckgeben, und setzen dieses mittels Zuweisung an _val als das neue generierte Attribut.
Um das Attribut der Regel string (und literal) muessen wir uns nicht kuemmern, da Spirit automatisch ein std::string Attribut erzeugt, wenn wir std::string als Rueckgabewert der Regel festlegen (die Typen sind kompatibel, d.h. es existiert bereits eine Transformationsvorschrift, die das Attribut der Regel in einen String umwandeln kann – dies ist fuer benutzerdefinierte Typen natuerlich leider nie der Fall).
Betrachten wir als naechstes die listDefinition. Fuer diese muessen wir mehrere Aktionen verwenden. Zu Beginn der Regel muss ein neues Objekt vom Typ list (aus unserer Objekthierarchie) angelegt werden, das wir in _val zwischenspeichern. Fuer jede nun folgende groupDefinition, listDefinition, arrayDefinition oder atom muss das zugehoerige Attribut an das list Objekt angehaengt werden. Wir erinnern uns, dass wir dem list Objekt eine Funktion append(element*) gegeben hatten. Diese Funktion koennen wir allerdings in der semantischen Aktion nicht ohne weiteres verwenden, sondern muessen sie zunaechst in einen Struct kapseln:
struct append_impl { template <typename C, typename Arg> struct result { typedef void type; }; template <typename C, typename Arg> void operator()(C* c, const Arg& data) const { c->append(data); } }; phoenix::function<append_impl> const append = append_impl();
Damit machen wir das struct append_impl zu einem Funktor, der unter dem Namen append zur Verfuegung steht. Die gesamte arrayDefinition Regel koennen wir dann folgendermassen schreiben:
arrayDefinition = lit('[') [_val = new_<cfg::list>()] >> -( double_ [append(_val, new_<cfg::atom>(_1))] % ',' | long_ [append(_val, new_<cfg::atom>(_1))] % ',' | bool_ [append(_val, new_<cfg::atom>(_1))] % ',' | string [append(_val, new_<cfg::atom>(_1))] % ',' ) > ']' ;
Diese Folge von Aktionen erzeugt also zunaechst ein neues Objekt vom Typ list und ruft dann fuer jedes Auftreten von double, long, bool oder string Werten den append Funktor auf, der dem list Objekt (in _val) ein neues atom anfuegt, dass in-place erzeugt wird. Wer sich nun fragt, warum wir das append vier Mal geschrieben haben, statt es einfach einmal hinter dem geklammerten Ausdruck zu verwenden, der moege bitte den Listenoperator am Ende der Zeilen beachten. Dieser fuehrt, zusammen mit dem “-” Operator fuer einen optionalen Ausdruck zu einem generierten Attribut mit komplexem Typ (optional< variant<...> >, siehe Beginn des Artikels), fuer das wir sicherlich keine append Funktion haetten schreiben wollen. Dadurch, dass wir die semantischen Aktionen direkt mit den POD Typen assoziieren, koennen wir einfach den Konstruktor von atom benutzen, um ein zugehoeriges Objekt zu erzeugen und fuegen dieses direkt in die Liste ein.
Die Regel listDefinition wird aehnlich implementiert, jedoch muessen wir in diesem Fall kein neues atom erzeugen, sondern koennen das generierte Attribut der Unterregel an den append Aufruf durchschleifen. Die genaue Implementierung ist dem vollstaendigen Sourcecode zu entnehmen.
Kommen wir zur groupDefinition: Hier muessen wir natuerlich zunaechst ein group Objekt erzeugen (wozu wir die lit Regel missbrauchen), und anschliessend saemtliche enthaltenen variableAssignments einfuegen. Hierbei habe ich mich fuer eine etwas andere Loesung entschieden, die ein vererbtes Attribut benutzt. Das bedeutet, dass eine Referenz auf eine lokale Variable an eine Subregel uebergeben wird, die das vererbte Attribut verwenden und ggf. auch modifizieren kann:
groupDefinition = lit('{') [_val = new_<cfg::group>()] >> *( variableAssignment(_val) ) > '}' ;
Das Einfuegen der variableAssignments geschieht also nicht in der Regel groupDefinition, sondern in variableAssignment selbst. Die Regel file kann analog implementiert werden, allerdings haben wir hier kein lit zur Verfuegung, an dass wir die Erzeugung des group Objekts anheften koennen und muessen deshalb eine Dummyregel (eps) verwenden, die keinerlei Auswirkung auf die Grammatik hat (siehe Sourcecode).
Die letzte Regel, die wir genauer analysieren wollen, ist variableAssignment. Diese bekommt das bereits erwaehnte vererbte Attribut uebergeben (dessen Platzhalter _r1 heisst), und sieht so aus:
variableAssignment = literal [_a = _1] >> '=' >> ( groupDefinition [insert(_r1, _a, _1)] | listDefinition [insert(_r1, _a, _1)] | arrayDefinition [insert(_r1, _a, _1)] | atom [insert(_r1, _a, _1)] ) > ';' ;
Der Funktor insert wird dabei aehnlich wie append implementiert. Um den Namen der Variablen zwischenzuspeichern, benutzen wir eine lokale Variable vom Typ string, deren Platzhalter _a ist (weitere lokale Variablen wuerden _b, _c, usw. heissen). Der Typ von _a muss in der Deklaration der Regel angegeben werden (siehe Sourcecode).
Damit haben wir unseren Parser komplettiert und koennen nun noch eine Klasse cfg_file schreiben, die das Oeffnen der Datei und die Verwendung des Parsers kapselt. Der vollstaendige Code inklusive einem kleinen Testprogramm kann hier heruntergeladen werden:
cfg_file library v1.0 (cfg_file_1_0.tar.gz)
Ich hoffe Ihnen hat dieses kleine Tutorial gefallen. Fuer weitere Fragen und Kritik oder Anregungen stehe ich gerne zur Verfuegung.