Die Boost C++ Bibliotheken


Kapitel 12: Parser


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.

Die englische Übersetzung dieses Buchs ist im Buchhandel verfügbar! Es handelt sich dabei um eine aktualisierte Version, die auf den Boost C++ Bibliotheken 1.47.0 vom Juli 2011 basiert. So wurde das Buch im Hinblick auf neue Versionen verschiedener Boost Bibliotheken aktualisiert (zum Beispiel auf Boost.Spirit 2.x, Boost.Signals 2 und Boost.Filesystem 3). Mit insgesamt 38 Boost Bibliotheken werden außerdem mehr Bibliotheken als je zuvor vorgestellt (neu sind zum Beispiel Boost.CircularBuffer, Boost.Intrusive und Boost.MultiArray). Das Buch kann bei Amazon oder anderen Buchhändlern bestellt werden. Zusätzliche Informationen zum Buch finden Sie beim Verlag XML Press.


12.1 Allgemeines

Parser werden verwendet, um Formate zu lesen, die eine flexible und daher möglicherweise komplizierte Strukturierung von Daten ermöglichen. Ein gutes Beispiel für ein derartiges Format ist C++-Code. Der Parser Ihres Compilers muss in der Lage sein, die vielen Sprachkonstrukte der Programmiersprache C++ in all ihren möglichen Kombinationen zu erkennen, damit sie dann in Binärcode übersetzt werden können.

Das Problem in der Entwicklung von Parsern ist, dass je nach Format eine unüberschaubar große Zahl an Regeln zu beachten sind, nach denen Daten strukturiert sein können. So unterstützt C++ derart viele Sprachkonstrukte, dass in der Entwicklung eines entsprechenden Parsers unzählige if-Überprüfungen notwendig wären, um jeden noch so erdenklichen C++-Code auch als gültigen Code zu erkennen.

Die Bibliothek Boost.Spirit, die in diesem Kapitel vorgestellt wird, dreht den Spieß um. Anstatt explizite Regeln, wie sie für C++ beispielsweise aus der Dokumentation des Standards entnommen werden können, in Code zu übersetzen und mit unzähligen if-Anweisungen Quellcode auf diese Regeln zu überprüfen, bietet Boost.Spirit an, die Regeln in der sogenannten erweiterten Backus-Naur-Form niederzuschreiben. Boost.Spirit übernimmt dann den Rest und parst eine Datei, die C++-Quellcode enthält, gemäß den in der erweiterten Backus-Naur-Form ausgedrückten Regeln.

Die Grundidee von Boost.Spirit ähnelt der von regulären Ausdrücken. Anstatt einen Text mit vielen if-Anweisungen nach einem Muster zu durchsuchen, wird das gesuchte Muster als regulärer Ausdruck angegeben. Die Suche wird dann von einer Bibliothek wie Boost.Regex ausgeführt, ohne dass man sich als Entwickler um die Details kümmern muss.

In diesem Kapitel wird Ihnen gezeigt, wie Sie Boost.Spirit verwenden können, um komplizierte Formate zu lesen, für die reguläre Ausdrücke nicht mehr praktikabel sind. Da Boost.Spirit eine recht umfangreiche Bibliothek ist, die verschiedene Konzepte einführt, wird in diesem Kapitel schrittweise ein einfacher Parser für das JSON-Format erstellt. Es handelt sich dabei um ein tatsächlich existierendes Format, das unter anderem in Ajax-Anwendungen eingesetzt wird, um Daten ähnlich wie XML zwischen möglicherweise auf unterschiedlichen Plattformen laufenden Anwendungen auszutauschen.

Auch wenn Boost.Spirit die Entwicklung von Parsern vereinfacht, so ist es noch niemandem gelungen, basierend auf dieser Bibliothek einen C++-Parser zu entwickeln. Die Entwicklung eines Parsers für C++-Code mit Boost.Spirit bleibt ein langfristiges Ziel und soll eines Tages möglich sein. Aufgrund der Komplexität der Programmiersprache C++ ist dieses Ziel bisher nicht erreicht worden. Für derart komplexe Formate oder auch für Binärformate eignet sich Boost.Spirit demnach nicht.


12.2 Erweiterte Backus-Naur-Form

Die Backus-Naur-Form, abgekürzt BNF, ist eine Sprache, um Regeln zu beschreiben. Sie wird in vielen technischen Spezifikationen verwendet, da mit ihr Regeln präzise ausgedrückt werden können. So enthalten zum Beispiel viele Spezifikationen zahlreicher Internet-Protokolle, die sogenannten Requests for Comments, neben Erläuterungen in Textform Regeln in der BNF.

Die Bibliothek Boost.Spirit unterstützt die erweiterte Backus-Naur-Form, kurz EBNF. Es handelt sich hierbei insofern um eine Erweiterung als dass sich einige Regeln kürzer ausdrücken lassen als dies mit der BNF möglich wäre. Der Vorteil der EBNF ist also letztendlich eine verkürzte und damit auch vereinfachte Schreibweise.

Beachten Sie, dass es genaugenommen verschiedene Varianten der EBNF gibt, die sich in der Syntax unterscheiden können. In diesem Kapitel wie auch von Boost.Spirit wird die EBNF verwendet, deren Syntax der von regulären Ausdrücken ähnelt.

Da Boost.Spirit die Definition von Regeln in der EBNF voraussetzt, müssen Sie die EBNF kennen, um Boost.Spirit verwenden zu können. Häufig kennen Entwickler die EBNF bereits und entscheiden sich typischerweise für Boost.Spirit, weil es mit dieser Bibliothek möglich ist, in EBNF niedergeschreibene Regeln wiederzuverwenden. Da Kenntnisse in der EBNF Voraussetzung für den Einsatz von Boost.Spirit sind, erhalten Sie im Folgenden eine kurze Einführung. Wenn Sie die EBNF bereits kennen, aber in aller Kürze einen Überblick über die Schreibweise erhalten möchten, die in diesem Kapitel und von Boost.Spirit verwendet wird, finden Sie am Ende der XML-Spezifikation des W3C eine knappe Zusammenfassung der Syntax.

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

Die EBNF bezeichnet Regeln genaugenommen als Produktionsregeln. Dabei können beliebig viele Produktionsregeln zusammengefasst werden, um ein Format zu beschreiben. Das oben beschriebene Format besteht aus lediglich einer Produktionsregel. Sie definiert ein digit, das aus einer Zahl zwischen 0 und 9 besteht.

Definitionen wie digit werden Nichtterminalsymbole genannt. Diesen gegenüber stehen Terminalsymbole. Im obigen Beispiel sind die Zahlen 0 bis 9 Terminalsymbole. Sie können sie leicht erkennen, weil sie alle in Anführungszeichen stehen. Das bedeutet, dass die Zeichen keine besondere Bedeutung besitzen, sondern einfach nur Zeichen sind.

Die Zahlen in Anführungszeichen sind durch vertikale Striche verbunden. Der vertikale Strich ist ein Operator und hat die gleiche Bedeutung wie der Operator || in C++: Er stellt eine Alternative dar.

Zusammengefasst besagt die obige Produktionsregel, dass eine Ziffer zwischen 0 und 9 ein digit ist.

integer = ("+" | "-")? digit+

Das neue Nichtterminalsymbol integer besteht aus mindestens einem digit, dem optional ein Plus- oder Minuszeichen vorangestellt sein kann.

In der Definition von integer werden einige neue Operatoren verwendet. Mit runden Klammern können wie in der Mathematik Teilausdrücke gebildet werden, so dass auf diese Teilausdrücke andere Operatoren angewandt werden können. So bezieht sich das Fragezeichen auf den gesamten geklammerten Ausdruck. Das Fragezeichen bedeutet, dass der entsprechende Ausdruck entweder nicht oder genau einmal angegeben werden darf.

Das Pluszeichen hinter digit gibt an, dass der entsprechende Ausdruck mindestens einmal angegeben werden muss.

Die neue Produktionsregel definiert eine beliebige positive oder negative Ganzzahl. Während ein digit jeweils aus genau einer Ziffer besteht, können für ein integer beliebig viele Ziffern kombiniert werden, denen sogar ein Vorzeichensymbol vorangestellt werden darf. Während also zum Beispiel 5 ein digit und ein integer ist, ist +5 ausschließlich ein integer. Ebenso sind Zahlen wie 169 oder -8 ausschließlich integer.

Durch die Definition und Kombination von Nichtterminalsymbolen können immer kompliziertere Produktionsregeln erstellt werden.

real = integer "." digit*

Während die Definition von integer Ganzzahlen entspricht, umfasst die neue Definition von real Kommazahlen. Dazu wird auf die bereits definierten Nichtterminalsymbole integer und digit zugegriffen, die durch einen Punkt voneinander getrennt werden. Das Sternchen, das hinter digit angegeben ist, bedeutet, dass die Ziffern hinter dem Punkt optional sind: Es können beliebig viele Ziffern angegeben werden, aber auch gar keine.

Auf die Definition von real treffen demnach Kommazahlen wie 1.2, -16.99 oder auch 3. zu. Die obige Definition erlaubt jedoch keine Kommazahlen ohne führende Null wie beispielsweise .9.

Wie zu Beginn dieses Kapitels angekündigt soll Boost.Spirit verwendet werden, um einen Parser für das JSON-Format zu entwickeln. Zu diesem Zweck ist es notwendig, die Regeln, auf denen das JSON-Format basiert, in der EBNF auszudrücken.

object = "{" member ("," member)* "}"
member = string ":" value
string = '"' character* '"'
value = string | number | object | array | "true" | "false" | "null"
number = integer | real
array = "[" value ("," value)* "]"
character = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"

Das JSON-Format basiert auf Objekten. Diese enthalten in geschweiften Klammern Paare von Schlüsseln und Werten. Während die Schlüssel einfach nur Strings sind, können Werte Strings, Zahlen, Arrays, andere Objekte oder die Literalwerte true, false oder null sein. Strings sind beliebige Aneinanderreihungen von Zeichen, die in Anführungszeichen eingeschlossen sein müssen. Zahlen wiederum können Ganz- oder Kommazahlen sein. Arrays enthalten zwischen eckigen Klammern Werte, die durch Kommas getrennt werden.

Beachten Sie, dass die obige Definition nicht vollständig ist. Zum einen fehlen in der Definition von character beispielsweise Großbuchstaben und andere Zeichen. Zum anderen unterstützt JSON Besonderheiten wie Unicode oder Kontrollzeichen. All dies kann im Moment ignoriert werden, da Boost.Spirit häufig verwendete Nichtterminalsymbole für zum Beispiel alphanumerische Zeichen vordefiniert und somit keine endlosen Buchstabenketten getippt werden müssen. Außerdem wird später im Code ein String als eine Aneinanderreihung beliebiger Zeichen außer Anführungszeichen definiert. Da mit dem Anführungszeichen der String beendet wird, können somit wirklich alle anderen Zeichen in einem String verwendet werden. Der Grund, warum dies in obiger EBNF nicht entsprechend ausgedrückt wird, ist, dass in der EBNF zur Definition einer Ausnahme ein Nichtterminalsymbol für alle Zeichen definiert werden muss, von denen dann die ausgenommenen Zeichen ausgeklammert werden.

Im Folgenden sehen Sie ein Beispiel für ein JSON-Format, für das obige Definition zutrifft.

{
  "Boris Schäling" :
  {
    "Male": true,
    "Programming Languages": [ "C++", "Java", "C#" ],
    "Age": 31
  }
}

Das globale Objekt ist gekennzeichnet durch die äußeren geschweiften Klammern. In diesem Objekt befindet sich ein Schlüssel-Wert-Paar: Der Schlüssel hat den Namen "Boris Schäling", der Wert ist ein neues Objekt. In diesem neuen Objekt sind mehrere Schlüssel-Wert-Paare enthalten. Während alle Schlüssel wie gewohnt Strings sind, handelt es sich bei den Werten um den Literalwert true, ein Array bestehend aus mehreren Strings und eine Zahl.

Mit Boost.Spirit können Sie nun die oben definierten Regeln in der EBNF wiederverwenden, um einen Parser zu entwickeln, der das obige JSON-Format lesen kann.


12.3 Grammatik

Nachdem im vorherigen Abschnitt die Regeln, nach denen das JSON-Format gebildet wird, in der EBNF niedergeschrieben wurden, müssen sie irgendwie im Zusammenhang mit Boost.Spirit verwendet werden. Boost.Spirit ermöglicht nun, EBNF-Regeln als C++-Code niederzuschreiben. Das funktioniert, weil Boost.Spirit die verschiedenen Operatoren, die in der EBNF verwendet werden, überlädt.

Beachten Sie, dass die EBNF-Regeln leicht umgeschrieben werden müssen, damit gültiger C++-Code entsteht. So müssen Symbole, die in der EBNF lediglich durch Leerzeichen getrennt hintereinander stehen, durch irgendeinen Operator in C++ verbunden werden. Auch Operatoren wie das Sternchen, das Fragezeichen und das Pluszeichen, die in der EBNF jeweils hinter dem entsprechenden Symbol gesetzt werden, werden in C++ vor das jeweilige Symbol gesetzt, um als unäre Operatoren verwendet werden zu können.

Im Folgenden sehen Sie, wie die EBNF-Regeln des JSON-Formats in C++-Code für Boost.Spirit ausgedrückt werden.

#include <boost/spirit.hpp> 

struct json_grammar 
  : public boost::spirit::grammar<json_grammar> 
{ 
  template <typename Scanner> 
  struct definition 
  { 
    boost::spirit::rule<Scanner> object, member, string, value, number, array; 

    definition(const json_grammar &self) 
    { 
      using namespace boost::spirit; 
      object = "{" >> member >> *("," >> member) >> "}"; 
      member = string >> ":" >> value; 
      string = "\"" >> *~ch_p("\"") >> "\""; 
      value = string | number | object | array | "true" | "false" | "null"; 
      number = real_p; 
      array = "[" >> value >> *("," >> value) >> "]"; 
    } 

    const boost::spirit::rule<Scanner> &start() 
    { 
      return object; 
    } 
  }; 
}; 

int main() 
{ 
} 

Um die verschiedenen Klassen von Boost.Spirit verwenden zu können, müssen Sie die Headerdatei boost/spirit.hpp einbinden. Die Klassen aus Boost.Spirit stehen dann im Namensraum boost::spirit zur Verfügung.

Wenn Sie einen Parser mit Boost.Spirit entwickeln, müssen Sie eine sogenannte Grammatik erstellen. Diese definiert unter anderem die Regeln, nach denen Daten strukturiert werden. So wurde im obigen Beispiel eine Klasse json_grammar entwickelt, die von der Template-Klasse boost::spirit::grammar abgeleitet und mit dem Namen der Klasse instantiiert wurde. Die Klasse json_grammar wird also die gesamte zum Verständnis von JSON-Formaten notwendige Grammatik definieren.

Ein wichtiger Bestandteil der Grammatik sind wie bereits erwähnt die Regeln, um strukturierte Daten richtig lesen zu können. Diese Regeln werden in einer inneren Klasse namens definition definiert - dieser Name ist zwingend. Bei dieser Klasse handelt sich außerdem um ein Template mit einem Parameter. Die Klasse wird von Boost.Spirit mit einem sogenannten Scanner instantiiert. Der Scanner ist ein Konzept, das von Boost.Spirit intern verwendet wird. Die Klasse definition muss zwar ein Template sein, das als Parameter einen Scanner-Typ erwartet. Was Scanner genau sind und warum Boost.Spirit sie definiert ist für den alltäglichen Einsatz von Boost.Spirit aber unerheblich.

Die Klasse definition muss eine Methode start() definieren, die von Boost.Spirit aufgerufen wird, um das gesamte Regelwerk der Grammatik zu erhalten. Der Rückgabewert dieser Methode ist eine konstante Referenz auf boost::spirit::rule. Es handelt sich dabei ebenfalls um eine Template-Klasse, die mit dem Scanner-Typ instantiiert wird.

Die Klasse boost::spirit::rule wird verwendet, um Regeln zu definieren. Sie greifen immer dann auf diese Klasse zu, wenn Sie ein Nichtterminalsymbol definieren möchten. So tauchen die im vorherigen Abschnitt definierten Nichtterminalsymbole object, member, string, value, number und array alle als Objekte vom Typ boost::spirit::rule auf.

Alle diese Objekte sind als Eigenschaften der Klasse definition definiert. Das ist nicht zwingend erforderlich, erleichtert aber vor allem dann die Definition, wenn sich Regeln rekursiv aufeinander beziehen. Wie Sie bereits anhand der EBNF-Beispiele im vorherigen Abschnitt gesehen haben, sind rekursive Beziehungen kein Problem.

Betrachten Sie nun die Definition der Regeln im Konstruktor von definition. Auf den ersten Blick sollten sie den Produktionsregeln der EBNF aus dem vorherigen Abschnitt ähneln. Schließlich ist genau das das Ziel von Boost.Spirit, in EBNF definierte Produktionsregeln wiederverwenden zu können.

Während der C++-Code den in der EBNF erstellten Regeln ähnlich sieht, gibt es einige Besonderheiten. So sind alle Symbole mit dem >>-Operator verknüpft. Außerdem stehen EBNF-Operatoren wie das Sternchen nun vor und nicht mehr hinter Symbolen. Diese Änderungen sind natürlich notwendig, damit gültiger C++-Code geschrieben werden kann. Abgesehen von diesen Änderungen, die der Syntax von C++ geschuldet sind, bemüht sich Boost.Spirit jedoch, EBNF-Regeln möglichst ohne große Änderungen direkt in C++-Code übernehmen zu können.

Im Konstruktor von definition werden zwei Klassen verwendet, die von Boost.Spirit zur Verfügung gestellt werden. Es handelt sich hierbei um boost::spirit::ch_p und boost::spirit::real_p. Boost.Spirit stellt häufig benötigte Regeln in Form von Parsern zur Verfügung, die wiederverwendet werden können. So können beliebige Zahlen - positive und negative Ganz- und Kommazahlen - mit boost::spirit::real_p erfasst werden, ohne dass Nichtterminalsymbole wie digit oder real selbst definiert werden müssen.

Die Klasse boost::spirit::ch_p kann verwendet werden, um einen Parser für ein einzelnes Zeichen zu erstellen. Das ist gleichbedeutend wie wenn dieses eine Zeichen in Anführungszeichen gesetzt werden würde. Der Grund, warum im obigen Beispiel dennoch boost::spirit::ch_p verwendet werden muss, ist, weil auf das Anführungszeichen die beiden Operatoren Tilde und Sternchen angewandt werden. Ohne die Klasse boost::spirit::ch_p würde dort *"\"" stehen, was der C++-Compiler als ungültigen Code zurückweisen würde.

Die Tilde ermöglicht übrigens den im vorherigen Abschnitt erwähnten Trick: Indem die Tilde dem Anführungszeichen vorangestellt wird, werden alle Zeichen außer dem Anführungszeichen akzeptiert.

Nachdem die Regeln zum Erkennen des JSON-Formats definiert wurden, können Sie sie wie im folgenden Beispiel zu sehen anwenden.

#include <boost/spirit.hpp> 
#include <fstream> 
#include <sstream> 
#include <iostream> 

struct json_grammar 
  : public boost::spirit::grammar<json_grammar> 
{ 
  template <typename Scanner> 
  struct definition 
  { 
    boost::spirit::rule<Scanner> object, member, string, value, number, array; 

    definition(const json_grammar &self) 
    { 
      using namespace boost::spirit; 
      object = "{" >> member >> *("," >> member) >> "}"; 
      member = string >> ":" >> value; 
      string = "\"" >> *~ch_p("\"") >> "\""; 
      value = string | number | object | array | "true" | "false" | "null"; 
      number = real_p; 
      array = "[" >> value >> *("," >> value) >> "]"; 
    } 

    const boost::spirit::rule<Scanner> &start() 
    { 
      return object; 
    } 
  }; 
}; 

int main(int argc, char *argv[]) 
{ 
  std::ifstream fs(argv[1]); 
  std::ostringstream ss; 
  ss << fs.rdbuf(); 
  std::string data = ss.str(); 

  json_grammar g; 
  boost::spirit::parse_info<> pi = boost::spirit::parse(data.c_str(), g, boost::spirit::space_p); 
  if (pi.hit) 
  { 
    if (pi.full) 
      std::cout << "parsing all data successfully" << std::endl; 
    else 
      std::cout << "parsing data partially" << std::endl; 
    std::cout << pi.length << " characters parsed" << std::endl; 
  } 
  else 
    std::cout << "parsing failed; stopped at '" << pi.stop << "'" << std::endl; 
} 

Boost.Spirit bietet eine freistehende Funktion boost::spirit::parse() an. Indem eine Grammatik instantiiert wird, wird ein Parser erstellt. Dieser wird als zweiter Parameter an die Funktion boost::spirit::parse() übergeben. Der erste Parameter ist der Text, der geparst werden soll. Der dritte Parameter ist ein Parser, der angibt, welche Zeichen im Text übersprungen werden sollen. Indem als dritter Parameter ein Objekt vom Typ boost::spirit::space_p angegeben wird, werden Leerzeichen ignoriert. Das bedeutet, dass zwischen zu erfassenden Daten - also überall da, wo der >>-Operator in den Regeln angewandt wurde - beliebig viele Leerzeichen stehen dürfen. Das schließt auch Tabulatoren und Zeilenumbrüche ein, was eine wesentlich flexiblere Schreibweise von Datenformaten ermöglicht.

Die Funktion boost::spirit::parse() gibt ein Objekt vom Typ boost::spirit::parse_info zurück. Dieses bietet vier Eigenschaften an, die Aufschluß darüber geben, ob der Text erfolgreich geparst werden konnte. Die Eigenschaft hit ist auf true gesetzt, wenn der Text erfolgreich geparst werden konnte. Konnten tatsächlich alle Zeichen im Text geparst werden, ohne dass zum Beispiel Leerzeichen am Ende des Textes übrigbleiben, ist zusätzlich full auf true gesetzt. Nur in dem Fall, wenn der Text erfolgreich geparst werden konnte und hit auf true gesetzt ist, ist length gültig und gibt die Anzahl der erfolgreich geparsten Zeichen zurück.

Sie dürfen nicht auf length zugreifen, wenn das Parsen nicht erfolgreich war. In diesem Fall können Sie lediglich über stop auf die Stelle im Text zugreifen, an der das Parsen abgebrochen wurde. Der Zugriff auf stop ist zwar auch dann erlaubt, wenn der Text erfolgreich geparst werden konnte. Viel Sinn macht dieser Zugriff aber nicht, da stop dann hinter den geparsten Text zeigt.


12.4 Aktionen

Bisher wissen Sie, wie Sie eine Grammatik definieren, um einen neuen Parser zu erhalten. Sie können den Parser anwenden, um herauszufinden, ob ein bestimmter Text gemäß den Regeln der Grammatik strukturiert ist. Wirklich lesen können Sie ein Datenformat jedoch noch nicht. Denn bisher passiert nichts mit den einzelnen Daten, die aus einem strukturierten Format wie JSON gelesen werden.

Um Daten, die vom Parser als auf eine Regel zutreffend identifiziert werden, verarbeiten zu können, müssen Aktionen verwendet werden. Es handelt sich dabei um Funktionen, die mit Regeln verknüpft werden. Entdeckt der Parser in einem Text Daten, auf die eine Regel zutrifft, wird automatisch die mit der Regel verknüpfte Aktion aufgerufen. Beim Aufruf werden die gefundenen Daten übergeben, so dass die Aktion diese verarbeiten kann. Sehen Sie sich dazu folgendes Beispiel an.

#include <boost/spirit.hpp> 
#include <string> 
#include <fstream> 
#include <sstream> 
#include <iostream> 

struct json_grammar 
  : public boost::spirit::grammar<json_grammar> 
{ 
  struct print 
  { 
    void operator()(const char *begin, const char *end) const 
    { 
      std::cout << std::string(begin, end) << std::endl; 
    } 
  }; 

  template <typename Scanner> 
  struct definition 
  { 
    boost::spirit::rule<Scanner> object, member, string, value, number, array; 

    definition(const json_grammar &self) 
    { 
      using namespace boost::spirit; 
      object = "{" >> member >> *("," >> member) >> "}"; 
      member = string[print()] >> ":" >> value; 
      string = "\"" >> *~ch_p("\"") >> "\""; 
      value = string | number | object | array | "true" | "false" | "null"; 
      number = real_p; 
      array = "[" >> value >> *("," >> value) >> "]"; 
    } 

    const boost::spirit::rule<Scanner> &start() 
    { 
      return object; 
    } 
  }; 
}; 

int main(int argc, char *argv[]) 
{ 
  std::ifstream fs(argv[1]); 
  std::ostringstream ss; 
  ss << fs.rdbuf(); 
  std::string data = ss.str(); 

  json_grammar g; 
  boost::spirit::parse_info<> pi = boost::spirit::parse(data.c_str(), g, boost::spirit::space_p); 
  if (pi.hit) 
  { 
    if (pi.full) 
      std::cout << "parsing all data successfully" << std::endl; 
    else 
      std::cout << "parsing data partially" << std::endl; 
    std::cout << pi.length << " characters parsed" << std::endl; 
  } 
  else 
    std::cout << "parsing failed; stopped at '" << pi.stop << "'" << std::endl; 
} 

Aktionen werden als Funktionen oder Funktionsobjekte implementiert. Wenn Sie eine Aktion in irgendeiner Weise initialisieren möchten oder Statusinformationen zwischen der wiederholten Ausführung einer Aktion speichern möchten, bieten sich Funktionsobjekte an. Im obigen Beispiel ist die hinzugefügte Aktion als Funktionsobjekt definiert worden.

Die Klasse print ist ein Funktionsobjekt, das Daten auf die Standardausgabe ausgibt. Der überladene Operator operator()() erhält hierzu beim Aufruf einen Zeiger auf den Anfang und das Ende der Daten, die von der Regel gefunden wurden, für die die Aktion ausgeführt wird.

Im obigen Beispiel wurde die Aktion mit dem Nichtterminalsymbol string verknüpft, das als erstes Symbol hinter member auftaucht. Dem Nichtterminalsymbol string ist dazu in eckigen Klammern eine Instanz vom Typ print übergeben worden. Da string an dieser Stelle den Schlüssel in Schüssel-Wert-Paaren von JSON-Objekten darstellt, wird also für jeden gefunden Schlüssel der überladene Operator operator()() der Klasse print aufgerufen, in der der Schlüssel auf die Standardausgabe ausgegeben wird.

Es ist nun möglich, beliebig viele Aktionen zu definieren oder Aktionen mit beliebig vielen Symbolen zu verknüpfen. Möchten Sie aber eine Aktion zum Beispiel mit einem Literalwert verknüpfen, müssen Sie aus dem gleichen Grund explizit einen Parser angeben, aus dem auch bei der Definition des Nichtterminalsymbol string die Klasse boost::spirit::ch_p verwendet wurde. So wird im nächsten Beispiel die Klasse boost::spirit::str_p eingesetzt, um ein Objekt vom Typ print mit dem Literalwert "true" zu verknüpfen.

#include <boost/spirit.hpp> 
#include <string> 
#include <fstream> 
#include <sstream> 
#include <iostream> 

struct json_grammar 
  : public boost::spirit::grammar<json_grammar> 
{ 
  struct print 
  { 
    void operator()(const char *begin, const char *end) const 
    { 
      std::cout << std::string(begin, end) << std::endl; 
    } 

    void operator()(const double d) const 
    { 
      std::cout << d << std::endl; 
    } 
  }; 

  template <typename Scanner> 
  struct definition 
  { 
    boost::spirit::rule<Scanner> object, member, string, value, number, array; 

    definition(const json_grammar &self) 
    { 
      using namespace boost::spirit; 
      object = "{" >> member >> *("," >> member) >> "}"; 
      member = string[print()] >> ":" >> value; 
      string = "\"" >> *~ch_p("\"") >> "\""; 
      value = string | number | object | array | str_p("true")[print()] | "false" | "null"; 
      number = real_p[print()]; 
      array = "[" >> value >> *("," >> value) >> "]"; 
    } 

    const boost::spirit::rule<Scanner> &start() 
    { 
      return object; 
    } 
  }; 
}; 

int main(int argc, char *argv[]) 
{ 
  std::ifstream fs(argv[1]); 
  std::ostringstream ss; 
  ss << fs.rdbuf(); 
  std::string data = ss.str(); 

  json_grammar g; 
  boost::spirit::parse_info<> pi = boost::spirit::parse(data.c_str(), g, boost::spirit::space_p); 
  if (pi.hit) 
  { 
    if (pi.full) 
      std::cout << "parsing all data successfully" << std::endl; 
    else 
      std::cout << "parsing data partially" << std::endl; 
    std::cout << pi.length << " characters parsed" << std::endl; 
  } 
  else 
    std::cout << "parsing failed; stopped at '" << pi.stop << "'" << std::endl; 
} 

Im obigen Beispiel wurde außerdem eine Aktion mit boost::spirit::real_p verknüpft. Während die meisten Parser einen Zeiger auf den Anfang und auf das Ende der gefundenen Daten übergeben, übergibt boost::spirit::real_p beim Aufruf der Aktion die gefundene Zahl als double. Das macht die Verarbeitung von Zahlen einfacher, da sie nicht selbst umgewandelt werden müssen. Damit jedoch ein Wert vom Typ double an die Aktion übergeben werden kann, wurde der Klasse print ein entsprechender überladener Operator operator()() hinzugefügt.

Neben den in diesem Kapitel kennengelerten Parsern wie boost::spirit::str_p oder boost::spirit::real_p bietet Boost.Spirit zahlreiche weitere an. So ist zum Beispiel boost::spirit::regex_p nützlich, wenn ein regulärer Ausdruck verwendet werden soll. Darüberhinaus stehen Parser zur Verfügung, um Bedingungen zu überprüfen oder Schleifen auszuführen. Dies ermöglicht es, dynamische Parser zu erstellen, die Daten in Abhängigkeit von Bedingungen unterschiedlich verarbeiten. Um einen Überblick davon zu bekommen, welche Hilfsmittel Boost.Spirit darüberhinaus anbietet, ist ein Blick in die Dokumentation dieser Bibliothek empfehlenswert.


12.5 Aufgaben

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Entwickeln Sie einen Taschenrechner, der beliebige Ganz- und Kommazahlen addieren und subtrahieren kann. Der Taschenrechner soll eine Eingabe wie =-4+8 + 1.5 akzeptieren und in diesem Fall die Lösung 5.5 ausgeben.