Home > Programmierung > Implementation eines Konfigurationsparsers mit Hilfe von boost::spirit (Teil 3a)

Implementation eines Konfigurationsparsers mit Hilfe von boost::spirit (Teil 3a)

Nachdem wir in Teil 1 und 2 einen Parser fuer unser Konfigurationsformat erstellt haben, wollen wir nun eine geeignete Objektstruktur implementieren, die uns zum einen erlaubt, eine hierarchische In-Memory-Abbildung unseres Formats zu schaffen und die zum anderen moeglichst leicht mit Hilfe von semantischen Aktionen durch den Parser erstellt werden kann.

Es gibt dabei prinzipiell mehrere Moeglichkeiten, wie vorgegangen werden kann. Meine erste Idee war, einen Baum aus polymorphen Objekten aufzubauen, der verschiedene Objekttypen fuer alle einzelnen Elemente der Konfigurationsstruktur enthaelt. Dies waere zwar fuer die Erstellung der Speicherstruktur durch den Parser am geeignetsten gewesen, haette jedoch den Benutzerzugriff auf die Daten unnoetig erschwert. In einem weiteren Prototyp waehlte ich als Datenstruktur einen monomorphen Baum aus nur einem einzigen Klassentyp, was allerdings Probleme bei der Abbildung der Hierarchie und der spaeteren Auswertung mit sich brachte.

Ich entschied mich daher fuer einen Kompromiss aus beiden Ansaetzen, bei dem ich die funktional gleichen Objekttypen zusammenfasse und in einen polymorphen Baum ueberfuehre. Intern wird dabei in Baum aus Zeigern erstellt (um eine hohe Performance zu gewaehrleisten), das externe Interface arbeitet jedoch ausschliesslich mit Objektreferenzen, so dass wir auch mit ueberladenen Operatoren arbeiten koennen.

Alle Konfigurationselemente erben gemeinsam von einer Klasse element, die wir zunaechst als leer definieren:

class element { };

Wir werden erst spaeter Funktionalitaet zu element hinzufuegen, wenn wir das externe Interface definieren.

Im naechsten Schritt muessen wir die hierarchiegenerierenden Elemente auf Klassen abbilden. Unser Format erlaubt Gruppen, Listen und Arrays. Listen und Arrays haben das gleiche Interface, sie unterscheiden sich lediglich in den zugelassenen Typen ihrer Subelemente. Da wir fuer dieses Tutorial keine Ruecktransformation unserer Objekthierarchie in eine Konfigurationsdatei benoetigen, koennen wir Liste und Array als gleichwertig betrachten und in einen Typ (list) zusammenfassen. Die Gruppe unterscheidet sich hingegen funktional von Liste und Array und wird deshalb getrennt abgebildet (group):

class group : public element
{
  void insert(const std::string& key, element* value)
  {
    std::string key_(key); settings_.insert(key_, value);
  }
 
  typedef boost::ptr_map<std::string, element> setting_map_t;
  setting_map_t settings_;
};
 
class list : public element
{
  void append(element* value) { settings_.push_back(value); }
 
  typedef boost::ptr_vector<element> setting_list_t;
  setting_list_t setting_;
};

Ich habe die insert bzw. append Funktionen absichtlich als private implementiert, da der Konfigurationsbaum fuer den Benutzer nicht modifizierbar sein soll. Den Parser werden wir spaeter als friend deklarieren.

Es fehlt nun nur noch eine Repraesentation der atomaren Elemente (Integer, Double, String und Bool). Wie bereits besprochen werden wir diese zu einer Klasse zusammenfassen:

class atom : public element
{
  explicit atom(const boost::variant<bool, long, double, std::string>& value) :
    value_(value)
  {}
 
  boost::variant<bool, long, double, std::string> value_;
};

Diese einfache Implementierung reicht bereits aus, um den Baum erzeugen zu koennen. Allerdings ist der Konfigurationsbaum recht nutzlos, wenn der Benutzer keine Moeglichkeit hat, diesen zu analysieren und einzelne Elemente zu extrahieren. Wir benoetigen also noch ein oeffentliches Interface, das uns eine Interaktion mit den Daten ermoeglicht.

Beginnen wir mit dem atom. Um auf den gekapselten Wert zugreifen zu koennen, koennten wir beispielsweise eine Funktion schreiben, die eine Kopie (oder konstante Referenz) des gespeicherten Variants zurueckgibt. Das hat jedoch den Nachteil, dass der Benutzer selbst boost::get aufrufen muss, um den Wert aus dem Variant zu extrahieren. Unser Ziel soll jedoch sein, die Verwendung der atom Klasse so transparent wie moeglich zu gestalten und die Implementierung nach aussen hin moeglichst zu verbergen. Daher fuegen wir besser der Klasse atom eine Funktion as() hinzu, die folgendermassen implementiert ist:

class atom : public element
{
  // ...
 
public:
  template <typename T>
  const T as() const
  {
    return boost::apply_visitor(atom_detail::visitor<T>(), value_);
  }
 
  // ...
};

Die Aufgabe des hier verwendeten Visitors ist nicht nur die Extraktion des gewuenschten Typs aus dem Variant, sondern gleichzeitig eine Konvertierung der zulaessigen Werte untereinander, um zum Beispiel einen Double Wert auch als String extrahieren zu koennen. Die Implementierung des Visitors ist etwas umfangreicher und nicht sonderlich interessant, daher verweise ich diesbezueglich auf den Quellcode, der im Teil 3b verlinkt ist.

Betrachten wir als naechstes die Klasse group. Sie benoetigt zum einen ein Interface, um ueber die Subelemente iterieren zu koennen, zum anderen eine Moeglichkeit, direkt das zugehoerige Element zu einem beliebigen Key extrahieren zu koennen. Beides ist einfach zu realisieren:

class group : public element
{
  // ...
 
public:  
  const element& get(const std::string& key) const
  {
    setting_map_t::const_iterator it = settings_.find(key);
    if(it == settings_.end())
      throw std::runtime_error("element not found");
    return *it->second;
  }
 
  typedef setting_map_t::const_iterator const_iterator;
  const_iterator begin() const { return settings_.begin(); }
  const_iterator end() const { return settings_.end(); }
 
  // ...
};

Fuer list implementieren wir die Iteratoren analog, statt der get Funktion mit einem String Key schreiben wir jedoch ein get mit Index:

class list : public element
{
  // ...
 
public:
  const element& get(size_t index) const { return settings_[index]; }
 
  // ...
};

Im naechsten Teil definieren wir das (umfangreichere) Interface fuer die Klasse element, die den zentralen Dreh- und Angelpunkt fuer den Zugriff auf die Konfigurationshierarchie darstellen wird.

KategorienProgrammierung Tags:
  1. Bisher keine Kommentare
  1. Bisher keine Trackbacks