Die Boost C++ Bibliotheken

Grammatik

Wenn Sie komplexere Formate parsen möchten und mehrere Regeln erstellen, die sich aufeinander beziehen, können Sie diese gruppieren. Boost.Spirit bietet hierfür die Klasse boost::spirit::qi::grammar an.

Beispiel 11.15. Regeln in einer Grammatik gruppieren
#include <boost/spirit/include/qi.hpp>
#include <boost/variant.hpp>
#include <string>
#include <vector>
#include <iostream>

using namespace boost::spirit;

template <typename Iterator, typename Skipper>
struct my_grammar : qi::grammar<Iterator,
  std::vector<boost::variant<int, bool>>(), Skipper>
{
  my_grammar() : my_grammar::base_type{values}
  {
    value = qi::int_ | qi::bool_;
    values = value % ',';
  }

  qi::rule<Iterator, boost::variant<int, bool>(), Skipper> value;
  qi::rule<Iterator, std::vector<boost::variant<int, bool>>(), Skipper>
    values;
};

struct print : public boost::static_visitor<>
{
  template <typename T>
  void operator()(T t) const
  {
    std::cout << std::boolalpha << t << ';';
  }
};

int main()
{
  std::string s;
  std::getline(std::cin, s);
  auto it = s.begin();
  my_grammar<std::string::iterator, ascii::space_type> g;
  std::vector<boost::variant<int, bool>> v;
  if (qi::phrase_parse(it, s.end(), g, ascii::space, v))
  {
    for (const auto &elem : v)
      boost::apply_visitor(print{}, elem);
  }
}

Beispiel 11.15 funktioniert wie Beispiel 11.14: Sie können Ganzzahlen und Wahrheitswerte durch Kommas getrennt in einer beliebigen Reihenfolge angeben, die vom Programm durch Semikolons getrennt ausgegeben werden. So greift auch dieses Beispiel auf die beiden Regeln value und values zu. In diesem Beispiel sind die Regeln zu einer Grammatik zusammengefasst. So ist eine Klasse my_grammar definiert, die von boost::spirit::qi::grammar abgeleitet ist.

Sowohl my_grammar als auch boost::spirit::qi::grammar sind Templates. my_grammar reicht die Template-Parameter an boost::spirit::qi::grammar weiter. Die Template-Parameter, die boost::spirit::qi::grammar erwartet, sind gleichbedeutend mit denjenigen von boost::spirit::qi::rule. So muss boost::spirit::qi::grammar mindestens ein Iteratortyp für einen String übergeben werden. Optional kann die Signatur einer Funktion übergeben werden, die den Typ des Attributs angibt, und der Typ eines Skippers.

In der Klasse my_grammar wird auf boost::spirit::qi::rule zugegriffen, um die Regeln value und values zu erstellen. Die Regeln sind als Eigenschaften definiert und werden im Konstruktor von my_grammar initialisiert.

Beachten Sie, dass die äußere Regel über base_type an den Konstruktor der Elternklasse übergeben werden muss. So weiß Boost.Spirit, welche Regel der Einstiegspunkt in die Grammatik ist.

Ist eine Grammatik definiert, kann sie wie ein Parser verwendet werden. So wird my_grammar in main() instanziiert und das entsprechende Objekt g an boost::spirit::qi::phrase_parse() übergeben.

Beispiel 11.16. Geparste Werte in Strukturen speichern
#include <boost/spirit/include/qi.hpp>
#include <boost/variant.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <string>
#include <vector>
#include <iostream>

using namespace boost::spirit;

typedef boost::variant<int, bool> int_or_bool;

struct int_or_bool_values
{
  int_or_bool first;
  std::vector<int_or_bool> others;
};

BOOST_FUSION_ADAPT_STRUCT(
  int_or_bool_values,
  (int_or_bool, first)
  (std::vector<int_or_bool>, others)
)

template <typename Iterator, typename Skipper>
struct my_grammar : qi::grammar<Iterator, int_or_bool_values(), Skipper>
{
  my_grammar() : my_grammar::base_type{values}
  {
    value = qi::int_ | qi::bool_;
    values = value >> ',' >> value % ',';
  }

  qi::rule<Iterator, int_or_bool(), Skipper> value;
  qi::rule<Iterator, int_or_bool_values(), Skipper> values;
};

struct print : public boost::static_visitor<>
{
  template <typename T>
  void operator()(T t) const
  {
    std::cout << std::boolalpha << t << ';';
  }
};

int main()
{
  std::string s;
  std::getline(std::cin, s);
  auto it = s.begin();
  my_grammar<std::string::iterator, ascii::space_type> g;
  int_or_bool_values v;
  if (qi::phrase_parse(it, s.end(), g, ascii::space, v))
  {
    print p;
    boost::apply_visitor(p, v.first);
    for (const auto &elem : v.others)
      boost::apply_visitor(p, elem);
  }
}

Beispiel 11.16 basiert auf dem vorherigen, erwartet jedoch die Eingabe mindestens zweier Werte. Die Regel in values lautet value >> ',' >> value % ','.

Die erste Komponente in values ist value, die zweite value % ','. Der zuerst geparste Wert muss in einem Objekt vom Typ boost::variant gespeichert werden. Die von der zweiten Komponente geparsten Werte müssen in einem Container gespeichert werden. Mit int_or_bool_values enthält das Beispiel eine entsprechende Struktur, die den beiden Komponenten der Regel values entspricht.

Um int_or_bool_values mit Boost.Spirit verwenden zu können, wird auf das Makro BOOST_FUSION_ADAPT_STRUCT zugegriffen, das von Boost.Phoenix zur Verfügung gestellt wird. Über dieses Makro kann die Struktur int_or_bool_values wie ein Tuple behandelt werden – ein Tuple mit zwei Werten vom Typ int_or_bool und std::vector<int_or_bool>. Weil dieses Tuple die richtige Anzahl an Werten mit den richtigen Typen besitzt, kann values mit einer Signatur int_or_bool_values() definiert werden. values wird den ersten geparsten Wert in der Eigenschaft first und die weiteren geparsten Werte in der Eigenschaft others speichern.

boost::spirit::qi::phrase_parse() wird als Attribut ein Objekt vom Typ int_or_bool_values übergeben. Wenn Sie das Programm ausführen und mindestens zwei Ganzzahlen oder Wahrheitswerte durch Kommas getrennt eingeben, werden diese im Attribut gespeichert und anschließend auf die Standardausgabe ausgegeben.

Anmerkung

Der Parser wurde im Beispiel im Vergleich zum vorherigen absichtlich abgewandelt und erwartet aus gutem Grund mindestens zwei Werte. Würde values mit value % ',' definiert sein, würde int_or_bool_values lediglich eine Eigenschaft besitzen. So würden alle Werte wie im vorherigen Beispiel in einem Vektor gespeichert werden können. In diesem Fall würde int_or_bool_values einem Tuple mit lediglich einem Wert entsprechen – etwas, was Boost.Spirit nicht unterstützt. Strukturen mit lediglich einer Eigenschaft führen zu einem Compilerfehler. Es existieren mehrere Workarounds für dieses Problem.

Aufgaben

  1. Entwickeln Sie einen Parser, der beliebig viele Ganzzahlen addieren und subtrahieren kann. Der Parser soll Eingaben wie 1+2-5+8 verarbeiten können und in diesem Fall das Ergebnis 6 ausgeben.

  2. Erweitern Sie Ihren Parser dahingehend, dass Berechnungen mit Kommazahlen durchgeführt werden können. Es soll außerdem möglich sein, Zahlen als Brüche anzugeben. Ihr überarbeiteter Parser soll Eingaben wie 1.2+6/5-0.9 verarbeiten können und in diesem Fall das Ergebnis 1.5 ausgeben.