Die Boost C++ Bibliotheken

Kapitel 25. Boost.PropertyTree

Boost.PropertyTree bietet mit boost::property_tree::ptree eine Baumstruktur an, um Schüssel/Wert-Paare zu speichern. Baumstruktur bedeutet, dass es einen Stamm mit beliebig vielen Ästen gibt und jeder Ast wiederum beliebig viele Zweige haben kann. Sie kennen die Baumstruktur zum Beispiel von Dateisystemen, die ein Hauptverzeichnis mit Unterverzeichnissen haben, die ihrerseits wiederum Unterverzeichnisse haben – und so weiter.

Um die Klasse boost::property_tree::ptree verwenden zu können, müssen Sie die Headerdatei boost/property_tree/ptree.hpp einbinden. Da diese Headerdatei alle anderen von Boost.PropertyTree definierten Headerdateien einbindet, müssen Sie sich um keine weiteren Headerdateien kümmern.

Beispiel 25.1. Datenzugriff auf boost::property_tree::ptree
#include <boost/property_tree/ptree.hpp>
#include <iostream>

using boost::property_tree::ptree;

int main()
{
  ptree pt;
  pt.put("C:.Windows.System", "20 files");

  ptree &c = pt.get_child("C:");
  ptree &windows = c.get_child("Windows");
  ptree &system = windows.get_child("System");
  std::cout << system.get_value<std::string>() << '\n';
}

Im Beispiel 25.1 wird boost::property_tree::ptree verwendet, um einen Pfad auf ein Verzeichnis zu speichern. Dazu wird die Methode put() aufgerufen. Diese Methode erwartet zwei Parameter, da es sich bei boost::property_tree::ptree um eine Baumstruktur handelt, die Schüssel/Wert-Paare speichert. Der Baum besteht nicht nur aus Ästen und Zweigen – Ästen und Zweigen muss ein Wert zugewiesen werden. Im Beispiel 25.1 ist das die Angabe 20 files.

Interessanter ist der erste an put() übergebene Parameter. Es handelt sich hierbei um einen Pfad auf ein Verzeichnis. Es wird jedoch nicht der unter Windows übliche Backslash verwendet, um Verzeichnisnamen zu trennen. Als Trennzeichen kommt ein Punkt zum Einsatz.

Es ist wichtig, dass der Punkt verwendet wird, da dieser das von Boost.PropertyTree definierte Trennzeichen für Schlüssel ist. Die Angabe C:.Windows.System führt dazu, dass pt einen Ast namens C: erhält, der wiederum einen Ast namens Windows besitzt, der wiederum einen Ast namens System hat. Der Punkt stellt sicher, dass die gewünschte verästelte Struktur mit drei Ebenen entsteht. Wäre hingegen C:\Windows\System als Schlüssel verwendet worden, hätte pt einen Ast namens C:\Windows\System erhalten, ohne dass dieser eigene Äste hätte.

Nach dem Aufruf von put() wird auf pt zugegriffen, um den gespeicherten Wert 20 files zu lesen und auf die Standardausgabe auszugeben. Dies geschieht, indem sich von Ast zu Ast – oder von Verzeichnis zu Verzeichnis – gehangelt wird.

Um auf einen Ast in der nächstliegenden untergeordneten Ebene zuzugreifen, muss die Methode get_child() verwendet werden. Sie gibt eine Referenz auf ein Objekt vom gleichen Typ zurück, für den get_child() aufgerufen wird – im Beispiel 25.1 eine Referenz auf boost::property_tree::ptree. Da jeder Ast seinerseits Äste haben kann und es keine strukturellen Unterschiede zwischen Ästen auf höheren und niedrigeren Ebenen gibt, wird der gleiche Typ verwendet.

Nach drei Aufrufen von get_child() wird der boost::property_tree::ptree erhalten, der dem Verzeichnis System entspricht. Um den Wert zu lesen, der zu Beginn des Beispiels mit put() gespeichert wurde, wird get_value() aufgerufen.

Beachten Sie, dass es sich bei get_value() um eine Template-Funktion handelt und Sie den Typ des Rückgabewerts als Parameter übergeben müssen. So kann get_value() automatisch eine Typumwandlung vornehmen.

Beispiel 25.2. Datenzugriff mit basic_ptree<std::string, int>
#include <boost/property_tree/ptree.hpp>
#include <utility>
#include <iostream>

int main()
{
  typedef boost::property_tree::basic_ptree<std::string, int> ptree;
  ptree pt;
  pt.put(ptree::path_type{"C:\\Windows\\System", '\\'}, 20);
  pt.put(ptree::path_type{"C:\\Windows\\Cursors", '\\'}, 50);

  ptree &windows = pt.get_child(ptree::path_type{"C:\\Windows", '\\'});
  int files = 0;
  for (const std::pair<std::string, ptree> &p : windows)
    files += p.second.get_value<int>();
  std::cout << files << '\n';
}

Im Beispiel 25.2 wurden zwei Änderungen vorgenommen, um Pfade auf Verzeichnisse und die Anzahl der Dateien in Verzeichnissen einfacher speichern zu können. Zum einen werden Pfade wie unter Windows gewohnt mit einem Backslash als Trennzeichen an put() übergeben. Zum anderen wird die Anzahl der Dateien als int-Zahl gespeichert.

Boost.PropertyTree verwendet standardmäßig den Punkt als Trennzeichen für Schlüssel. Möchten Sie ein anderes Zeichen wie beispielsweise den Backslash verwenden, übergeben Sie den Schlüssel nicht als String an put(). Stattdessen packen Sie ihn in ein Objekt vom Typ boost::property_tree::ptree::path_type. Dem Konstruktor dieser Klasse, die von boost::property_tree::ptree abhängt, kann nicht nur der Schlüssel übergeben werden, sondern als zweiter Parameter das Zeichen, das als Trennzeichen verwendet werden soll. Auf diese Weise ist es im Beispiel 25.2 möglich, einen Pfad wie C:\Windows\System zu verwenden, ohne Backslashs durch Punkte ersetzen zu müssen.

Die von Boost.Property angebotene Datenstruktur boost::property_tree::ptree basiert auf einer Template-Klasse boost::property_tree::basic_ptree. Weil Schlüssel und Werte häufig Strings sind, ist boost::property_tree::ptree vordefiniert. Sie können aber auf boost::property_tree::basic_ptree zugreifen und andere Typen für Schlüssel und Werte verwenden. Die im Beispiel 25.2 definierte Datenstruktur verwendet int für Werte, um die Anzahl der Dateien in einem Verzeichnis nicht als String speichern zu müssen, sondern als Zahl.

boost::property_tree::ptree stellt die von Containern bekannten Methoden begin() und end() zur Verfügung. Eine Besonderheit ist jedoch, dass Sie bei boost::property_tree::ptree ausschließlich über Äste in einer Ebene iterieren. Im Beispiel 25.2 findet die Iteration über die Unterverzeichnisse von C:\Windows stattt. Es ist nicht möglich, einen Iterator zu erhalten, der über sämtliche Äste in allen Ebenen iteriert.

In der for-Schleife im Beispiel 25.2 wird auf die Anzahl der Dateien in den Unterverzeichnissen von C:\Windows zugegriffen, um sie zu summieren. Als Ergebnis gibt das Programm 70 aus. Dabei wird nicht direkt auf Objekte vom Typ ptree zugegriffen. Stattdessen wird über Elemente vom Typ std::pair<std::string, ptree> iteriert. In first ist der Schlüssel des aktuellen Asts gespeichert. Das sind im Beispiel 25.2 System und Cursors. Über second wird auf das neuerliche Objekt vom Typ ptree zugegriffen, das mögliche Unterverzeichnisse enthält. Im Beispiel soll jedoch lediglich der Wert erhalten werden, der den Verzeichnissen System und Cursors zugeordnet ist. Dazu wird wie bereits im Beispiel 25.1 die Methode get_value() aufgerufen.

Die Datenstruktur boost::property_tree::ptree ist insofern bemerkenswert, als dass sie ausschließlich den Wert des aktuellen Asts speichert – nicht dessen Schlüsselnamen. Sie können über get_value() den Wert erhalten. Es gibt aber keine Methode, um den Schlüssel abzurufen. Dieser ist als Wert in der übergeordneten Datenstruktur boost::property_tree::ptree gespeichert, die die höhere Ebene repräsentiert. Das erklärt auch, warum in der for-Schleife auf Elemente vom Typ std::pair<std::string, ptree> zugegriffen wird.

Beispiel 25.3. Datenzugriff mit einem Translator
#include <boost/property_tree/ptree.hpp>
#include <boost/optional.hpp>
#include <iostream>
#include <cstdlib>

struct string_to_int_translator
{
  typedef std::string internal_type;
  typedef int external_type;

  boost::optional<int> get_value(const std::string &s)
  {
    char *c;
    long l = std::strtol(s.c_str(), &c, 10);
    return boost::make_optional(c != s.c_str(), static_cast<int>(l));
  }
};

int main()
{
  typedef boost::property_tree::iptree ptree;
  ptree pt;
  pt.put(ptree::path_type{"C:\\Windows\\System", '\\'}, "20 files");
  pt.put(ptree::path_type{"C:\\Windows\\Cursors", '\\'}, "50 files");

  string_to_int_translator tr;
  int files =
    pt.get<int>(ptree::path_type{"c:\\windows\\system", '\\'}, tr) +
    pt.get<int>(ptree::path_type{"c:\\windows\\cursors", '\\'}, tr);
  std::cout << files << '\n';
}

Im Beispiel 25.3 kommt mit boost::property_tree::iptree eine weitere von Boost.PropertyTree vordefinierte Datenstruktur zum Einsatz. Sie verhält sich grundsätzlich genauso wie boost::property_tree::ptree. Der einzige Unterschied ist, dass bei Schlüsseln nicht zwischen Groß- und Kleinschreibung unterschieden wird. So kann zum Beispiel ein Wert, der mit dem Schlüssel C:\Windows\System gespeichert wurde, über c:\windows\system gelesen werden.

Im Gegensatz zum Beispiel 25.1 wird nicht mehr nacheinander über get_child() auf Unterebenen zugegriffen. So wie mit put() ein Wert direkt geschrieben werden kann, kann mit get() ein Wert direkt gelesen werden. Die Angabe des Schlüssels erfolgt auf die gleiche Weise – also zum Beispiel auch über boost::property_tree::iptree::path_type.

Wie bei get_value() handelt es sich auch bei get() um eine Template-Funktion. Sie müssen den Typ des Rückgabewerts als Parameter angeben. Boost.PropertyTree führt automatisch eine Typkonvertierung durch.

Boost.PropertyTree verwendet für Typkonvertierungen Translatoren. Die Bibliothek stellt einige Translatoren zur Verfügung. Diese basieren auf Streams und können Typumwandlungen automatisch vornehmen.

Im Beispiel 25.3 wird ein Translator string_to_int_translator definiert, der einen Wert vom Typ std::string in ein int umwandelt. Der Translator wird als zusätzlicher Parameter an get() übergeben. Da er ausschließlich zum Lesen verwendet wird, besitzt er lediglich eine Methode get_value(). Würde er auch zum Schreiben verwendet werden – er würde dann als zusätzlicher Parameter an put() übergeben werden – müsste auch eine Methode put_value() definiert werden.

get_value() bekommt den Wert mit dem Typ übergeben, wie er in pt verwendet wird. Der Rückgabewert besitzt jedoch nicht allein den Typ, in den umgewandelt werden soll. Wie Sie sehen, wird auf boost::optional zugegriffen. Diese Klasse wird verwendet, weil eine Typumwandlung nicht immer gelingen muss. Würde im Beispiel 25.3 ein Wert gespeichert, der nicht mit std::strtol() in ein int umgewandelt werden könnte, müsste ein Objekt vom Typ boost::optional zurückgegeben werden, das leer ist.

Beachten Sie, dass ein Translator darüber hinaus zwei Typen internal_type und external_type definieren muss. Wenn Sie außerdem put_value() definieren möchten, um eine Typumwandlung auch beim Speichern von Daten vornehmen zu können, muss diese Methode ähnlich wie get_value() definiert werden.

Wenn Sie Beispiel 25.3 dahingehend ändern, dass zum Beispiel anstatt dem Wert 20 files lediglich 20 gespeichert wird, können Sie get_value() aufrufen, ohne einen Translator übergeben zu müssen. Die von Boost.PropertyTree definierten Translatoren können eine Typumwandlung von std::string zu int durchführen. Die Typumwandlung wird jedoch nur dann als erfolgreich abgeschlossen betrachtet, wenn der gesamte String umgewandelt werden konnte. Der String darf also keine Buchstaben enthalten, damit die mit Boost.PropertyTree ausgelieferten Translatoren verwendet werden können. Da std::strtol() eine Typumwandlung durchführen kann, solange ein String mit Ziffern beginnt, ist der Translator string_to_int_translator, der im Beispiel 25.3 zum Einsatz kommt, liberaler.

Beispiel 25.4. Verschiedene Methoden von boost::property_tree::ptree
#include <boost/property_tree/ptree.hpp>
#include <utility>
#include <iostream>

using boost::property_tree::ptree;

int main()
{
  ptree pt;
  pt.put("C:.Windows.System", "20 files");

  boost::optional<std::string> c = pt.get_optional<std::string>("C:");
  std::cout << std::boolalpha << c.is_initialized() << '\n';

  pt.put_child("D:.Program Files", ptree{"50 files"});
  pt.add_child("D:.Program Files", ptree{"60 files"});

  ptree d = pt.get_child("D:");
  for (const std::pair<std::string, ptree> &p : d)
    std::cout << p.second.get_value<std::string>() << '\n';

  boost::optional<ptree&> e = pt.get_child_optional("E:");
  std::cout << e.is_initialized() << '\n';
}

Sie können die Methode get_optional() verwenden, wenn Sie den Wert eines Schlüssels lesen wollen, aber nicht sicher sind, ob der Schlüssel existiert. get_optional() gibt den Wert in einem Objekt vom Typ boost::optional zurück, das leer ist, wenn der Schlüssel nicht gefunden wurde. Ansonsten funktioniert get_optional() wie get().

Die beiden Methoden put_child() und add_child() machen auf den ersten Blick das Gleiche wie put(). Der Unterschied ist, dass put() lediglich ein Schlüssel/Wert-Paar erstellt, während Sie mit put_child() und add_child() einen kompletten Baum in einen anderen hängen. Achten Sie auf den zweiten Parameter, der an put_child() und add_child() übergeben wird: Es handelt sich um ein Objekt vom Typ boost::property_tree::ptree.

Der Unterschied zwischen put_child() und add_child() ist, dass put_child() auf einen existierenden Schlüssel zugreift, sollte er bereits existieren. add_child() hingegen fügt dem Baum immer einen neuen Schlüssel hinzu. So hat der Baum im Beispiel 25.4 tatsächlich zwei Schlüssel D:.Program Files. Das kann je nach Anwendungsfall verwirrend sein. Stellt der Baum ein Verzeichnissystem dar, sollte es idealerweise nicht zwei gleichlautende Pfade geben. Da boost::property_tree::ptree gleichnamige Schlüssel erlaubt, müssen Sie in diesem Fall selbst darauf achten, nicht mehrere gleichnamige Schlüssel zu erstellen.

Wenn Sie Beispiel 25.4 ausführen, werden Ihnen in der for-Schleife die Werte ausgegeben, die den Schlüsseln unterhalb von D: zugeordnet sind. Das Beispiel gibt 50 files und 60 files aus, was beweist, dass es tatsächlich zwei gleichnamige Schlüssel D:.Program Files gibt.

Als letzte Methode wird Ihnen im Beispiel 25.4 get_child_optional() vorgestellt. Grundsätzlich verwenden Sie diese Methode wie get_child(). Diese Methode gibt ein Objekt vom Typ boost::optional zurück. Sie können get_child_optional() aufrufen, wenn Sie nicht sicher sind, ob ein Schlüssel existiert.

Beispiel 25.5. boost::property_tree::ptree im JSON-Format speichern
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <iostream>

using namespace boost::property_tree;

int main()
{
  ptree pt;
  pt.put("C:.Windows.System", "20 files");
  pt.put("C:.Windows.Cursors", "50 files");

  json_parser::write_json("file.json", pt);

  ptree pt2;
  json_parser::read_json("file.json", pt2);

  std::cout << std::boolalpha << (pt == pt2) << '\n';
}

Boost.PropertyTree bietet nicht nur einen Container an, um Daten im Speicher zu verwalten. Die Bibliothek stellt wie im Beispiel 25.5 zu sehen auch Funktionen zur Verfügung, um einen boost::property_tree::ptree in einer Datei zu speichern und von einer Datei zu laden.

Über die Headerdatei boost/property_tree/json_parser.hpp erhalten Sie Zugriff auf die Funktionen boost::property_tree::json_parser::write_json() und boost::property_tree::json_parser::read_json(). Diese ermöglichen es Ihnen, einen boost::property_tree::ptree im JSON-Format zu speichern und zu laden. So können Sie Konfigurationsdateien im JSON-Format unterstützen.

Wenn Sie auf Funktionen zugreifen möchten, die einen boost::property_tree::ptree in einer Datei speichern oder von einer Datei laden, müssen Sie Headerdateien wie boost/property_tree/json_parser.hpp explizit einbinden. Es reicht nicht aus, lediglich auf boost/property_tree/ptree.hpp zuzugreifen.

Neben boost::property_tree::json_parser::write_json() und boost::property_tree::json_parser::read_json() stellt Boost.PropertyTree Funktionen für weitere Datenformate zur Verfügung. Verwenden Sie boost::property_tree::ini_parser::write_ini() und boost::property_tree::ini_parser::read_ini() aus boost/property_tree/ini_parser.hpp, um INI-Dateien zu unterstützen. Mit boost::property_tree::xml_parser::write_xml() und boost::property_tree::xml_parser::read_xml() aus boost/property_tree/xml_parser.hpp können Sie Daten im XML-Format speichern und laden. Und mit boost::property_tree::info_parser::write_info() und boost::property_tree::info_parser::read_info() aus boost/property_tree/info_parser.hpp wird ein Format unterstützt, das explizit für Boost.PropertyTree entwickelt wurde und zur Serialisierung von Baumstrukturen optimiert ist.

Keines der vier unterstützten Formate garantiert, dass ein boost::property_tree::ptree nach dem Speichern und Laden genauso aussieht wie zuvor. Im JSON-Format kann zum Beispiel eine Typinformation verloren gehen, weil boost::property_tree::ptree nicht zwischen true und true unterscheiden kann. Der Typ des Werts ist immer derselbe. Auch wenn die vorgestellten Funktionen es sehr einfach machen, einen boost::property_tree::ptree zu speichern und zu laden, sollten Sie daran denken, dass Boost.PropertyTree die Formate nicht vollständig unterstützt. Das Augenmerk dieser Bibliothek liegt auf dem Container boost::property_tree::ptree und nicht auf der Unterstützung verschiedener Datenformate.

Aufgabe

Entwickeln Sie ein Programm, das diese JSON-Datei lädt und die Namen aller Tiere in die Standardausgabe schreibt. Ist die Log-Einstellung all auf true gesetzt, sollen nicht nur der Name, sondern alle Eigenschaften der Tiere in die Standardausgabe geschrieben werden:

{
  "animals": [
    {
      "name": "cat",
      "legs": 4,
      "has_tail": true
    },
    {
      "name": "spider",
      "legs": 8,
      "has_tail": false
    }
  ],
  "log": {
    "all": true
  }
}