Die Boost C++ Bibliotheken

Archive

Das zentrale Konzept in Boost.Serialization ist das Archiv. Das Archiv stellt die Byte-Sequenz dar, die serialisierte C++-Objekte repräsentiert. Objekte können einem Archiv hinzugefügt und damit serialisiert werden oder von einem Archiv geladen werden. Dies setzt voraus, dass die gleichen Typen verwendet werden. Nur dann können beim Laden die gleichen C++-Objekte erstellt werden, die zuvor gespeichert wurden.

Beispiel 64.1. boost::archive::text_oarchive in Aktion
#include <boost/archive/text_oarchive.hpp>
#include <iostream>

using namespace boost::archive;

int main()
{
  text_oarchive oa{std::cout};
  int i = 1;
  oa << i;
}

Boost.Serialization stellt mehrere Archiv-Klassen zur Verfügung. So ist in der Headerdatei boost/archive/text_oarchive.hpp das Archiv boost::archive::text_oarchive definiert. Dieses Archiv ermöglicht es, Objekte als Text-Stream zu serialisieren. So gibt Beispiel 64.1 22 serialization::archive 11 1 auf die Standardausgabe aus.

Sie können das Objekt oa vom Typ boost::archive::text_oarchive wie einen Stream verwenden und per operator<< eine Variable serialisieren. Sie sollten Archive jedoch nicht als herkömmliche Streams ansehen, in die Sie beliebig Daten ablegen können. Immerhin sollen die in einem Archiv serialisierten Daten später gelesen werden, was erfordert, dass beim Laden exakt die gleichen Typen verwendet werden und Daten in der richtigen Reihenfolge gelesen werden. Sehen Sie sich dazu Beispiel 64.2 an, in dem die Variable vom Typ int nicht nur serialisiert, sondern auch geladen wird.

Beispiel 64.2. boost::archive::text_iarchive in Aktion
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <iostream>
#include <fstream>

using namespace boost::archive;

void save()
{
  std::ofstream file{"archive.txt"};
  text_oarchive oa{file};
  int i = 1;
  oa << i;
}

void load()
{
  std::ifstream file{"archive.txt"};
  text_iarchive ia{file};
  int i = 0;
  ia >> i;
  std::cout << i << '\n';
}

int main()
{
  save();
  load();
}

Während die Klasse boost::archive::text_oarchive verwendet werden kann, um Daten als Text-Stream zu serialisieren, kann boost::archive::text_iarchive verwendet werden, um Daten von einem derartigen Stream wieder zu lesen. Um diese Klasse zu verwenden, muss die Headerdatei boost/archive/text_iarchive.hpp eingebunden werden.

Archive erwarten im Konstruktor als Parameter einen Input- oder Output-Stream. Archive greifen auf diese Streams zu, um serialisierte Daten dort auszugeben oder von dort zu laden. So wird im Beispiel 64.2 auf eine Datei zugegriffen. Sie können jedoch auch zum Beispiel einen Stringstream verwenden.

Beispiel 64.3. Serialisierung mit einem Stringstream
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <iostream>
#include <sstream>

using namespace boost::archive;

std::stringstream ss;

void save()
{
  text_oarchive oa{ss};
  int i = 1;
  oa << i;
}

void load()
{
  text_iarchive ia{ss};
  int i = 0;
  ia >> i;
  std::cout << i << '\n';
}

int main()
{
  save();
  load();
}

Beispiel 64.3 gibt wie das vorherige 1 auf die Standardausgabe aus. Im Gegensatz zum vorherigen Beispiel serialisiert es Daten nicht in einer Datei, sondern in einem Stringstream.

Sie wissen nun, wie Variablen primitiver Typen serialisiert werden. Im Beispiel 64.4 sehen Sie, wie die Serialisierung von Objekten benutzerdefinierter Typen erfolgt.

Beispiel 64.4. Serialisierung von benutzerdefinierten Typen mit einer Methode
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <iostream>
#include <sstream>

using namespace boost::archive;

std::stringstream ss;

class animal
{
public:
  animal() = default;
  animal(int legs) : legs_{legs} {}
  int legs() const { return legs_; }

private:
  friend class boost::serialization::access;

  template <typename Archive>
  void serialize(Archive &ar, const unsigned int version) { ar & legs_; }

  int legs_;
};

void save()
{
  text_oarchive oa{ss};
  animal a{4};
  oa << a;
}

void load()
{
  text_iarchive ia{ss};
  animal a;
  ia >> a;
  std::cout << a.legs() << '\n';
}

int main()
{
  save();
  load();
}

Wenn Objekte benutzerdefinierter Typen serialisiert werden sollen, müssen sie wie im Beispiel 64.4 eine Methode namens serialize() definieren. Diese Methode wird aufgerufen, wenn ein Objekt serialisiert wird oder von einem Byte-Stream geladen wird. Da serialize() sowohl zum Speichern als auch zum Laden verwendet wird, bietet Boost.Serialization neben operator<< und operator>> einen Operator an, der automatisch das Richtige tut. Wenn Sie den Operator operator& verwenden, müssen Sie in serialize() nicht zwischen Speichern und Laden unterscheiden.

Die Methode serialize() wird automatisch aufgerufen, wenn ein Objekt serialisiert oder geladen wird. Sie sollte niemals direkt aufgerufen werden und daher privat sein. Damit Boost.Serialization dennoch auf die Methode zugreifen kann, muss die Klasse boost::serialization::access als Freund deklariert werden.

Unter Umständen können Sie eine Klasse nicht ändern und ihr keine Methode serialize() hinzufügen. Dies trifft zum Beispiel auf Klassen aus der Standardbibliothek zu.

Beispiel 64.5. Serialisierung mit einer freistehenden Funktion
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <iostream>
#include <sstream>

using namespace boost::archive;

std::stringstream ss;

struct animal
{
  int legs_;

  animal() = default;
  animal(int legs) : legs_{legs} {}
  int legs() const { return legs_; }
};

template <typename Archive>
void serialize(Archive &ar, animal &a, const unsigned int version)
{
  ar & a.legs_;
}

void save()
{
  text_oarchive oa{ss};
  animal a{4};
  oa << a;
}

void load()
{
  text_iarchive ia{ss};
  animal a;
  ia >> a;
  std::cout << a.legs() << '\n';
}

int main()
{
  save();
  load();
}

Wenn Sie einen Typ serialisierbar machen wollen, den Sie nicht ändern können, können Sie wie im Beispiel 64.5 eine freistehende Funktion serialize() definieren. Als zweiter Parameter muss dieser Funktion eine Referenz auf ein Objekt des entsprechenden Typs übergeben werden.

serialize() als freistehende Funktion implementiert setzt voraus, dass von außerhalb ein Zugriff auf die wesentlichen Eigenschaften einer Klasse möglich ist. So kann serialize() im obigen Beispiel nur deswegen als freistehende Funktion implementiert werden, weil die Eigenschaft legs_ in der Klasse animal nicht mehr privat ist.

Für viele Klassen aus der Standardbibliothek stellt Boost.Serialization entsprechende serialize()-Funktionen zur Verfügung. Es müssen lediglich zusätzliche Headerdateien eingebunden werden, um Objekte zu serialisieren, die auf Klassen aus der Standardbibliothek basieren.

Beispiel 64.6. Strings serialisieren
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/string.hpp>
#include <iostream>
#include <sstream>
#include <string>
#include <utility>

using namespace boost::archive;

std::stringstream ss;

class animal
{
public:
  animal() = default;
  animal(int legs, std::string name) :
    legs_{legs}, name_{std::move(name)} {}
  int legs() const { return legs_; }
  const std::string &name() const { return name_; }

private:
  friend class boost::serialization::access;

  template <typename Archive>
  friend void serialize(Archive &ar, animal &a, const unsigned int version);

  int legs_;
  std::string name_;
};

template <typename Archive>
void serialize(Archive &ar, animal &a, const unsigned int version)
{
  ar & a.legs_;
  ar & a.name_;
}

void save()
{
  text_oarchive oa{ss};
  animal a{4, "cat"};
  oa << a;
}

void load()
{
  text_iarchive ia{ss};
  animal a;
  ia >> a;
  std::cout << a.legs() << '\n';
  std::cout << a.name() << '\n';
}

int main()
{
  save();
  load();
}

Im Beispiel 64.6 wird die Klasse animal um einen Namen vom Typ std::string erweitert. Damit diese Eigenschaft serialisiert werden kann, muss die Headerdatei boost/serialization/string.hpp eingebunden werden. Diese stellt die für std::string notwendige freistehende Funktion serialize() zur Verfügung.

Wie bereits erwähnt, definiert Boost.Serialization für zahlreiche Klassen aus der Standardbibliothek serialize()-Funktionen. Diese Funktionen sind in Headerdateien definiert, die den gleichen Namen tragen wie die entsprechenden Headerdateien aus der Standardbibliothek. Wenn Sie wie im Beispiel 64.6 Objekte vom Typ std::string serialisieren wollen, müssen Sie die Headerdatei boost/serialization/string.hpp einbinden. Wollen Sie ein Objekt vom Typ std::vector serialisieren, greifen Sie auf die Headerdatei boost/serialization/vector.hpp zu. Es ist also leicht erkennbar, welche Headerdateien Sie aus Boost.Serialization einbinden müssen.

Ein Parameter von serialize(), der bisher ignoriert wurde, ist version. Dieser Parameter ist dann von Bedeutung, wenn Sie davon ausgehen, dass Sie Ihr Programm im Laufe der Zeit weiterentwickeln und es abwärtskompatibel sein soll. So kann Beispiel 64.7 Archive laden, die mit Beispiel 64.5 erstellt wurden. Die Version von animal im Beispiel 64.5 enthält keinen Namen. Beispiel 64.7 überprüft beim Laden die Versionsnummer und greift nur dann auf einen Namen zu, wenn die Versionsnummer größer als 0 ist. So können Archive geladen werden, die mit einer älteren Programmversion erstellt wurden.

Beispiel 64.7. Abwärtskompatibilität mit Versionsnummern
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/string.hpp>
#include <iostream>
#include <sstream>
#include <string>
#include <utility>

using namespace boost::archive;

std::stringstream ss;

class animal
{
public:
  animal() = default;
  animal(int legs, std::string name) :
    legs_{legs}, name_{std::move(name)} {}
  int legs() const { return legs_; }
  const std::string &name() const { return name_; }

private:
  friend class boost::serialization::access;

  template <typename Archive>
  friend void serialize(Archive &ar, animal &a, const unsigned int version);

  int legs_;
  std::string name_;
};

template <typename Archive>
void serialize(Archive &ar, animal &a, const unsigned int version)
{
  ar & a.legs_;
  if (version > 0)
    ar & a.name_;
}

BOOST_CLASS_VERSION(animal, 1)

void save()
{
  text_oarchive oa{ss};
  animal a{4, "cat"};
  oa << a;
}

void load()
{
  text_iarchive ia{ss};
  animal a;
  ia >> a;
  std::cout << a.legs() << '\n';
  std::cout << a.name() << '\n';
}

int main()
{
  save();
  load();
}

Das Makro BOOST_CLASS_VERSION wird verwendet, um die Versionsnummer für eine Klasse zu setzen. So wird im Beispiel 64.7 die Versionsnummer für die Klasse animal auf 1 gesetzt. Standardmäßig – wenn BOOST_CLASS_VERSION nicht verwendet wird – ist die Versionsnummer 0.

Die Versionsnummer wird im Archiv gespeichert und ist Bestandteil jedes Archivs. Während beim Speichern die Versionsnummer verwendet wird, die mit BOOST_CLASS_VERSION für die entsprechende Klasse angegeben ist, wird beim Laden der Parameter version der Funktion serialize() auf den Wert gesetzt, der im Archiv gespeichert ist. Wenn die neue Version der Klasse animal auf ein Archiv zugreifen würde, in dem ein Objekt basierend auf der alten Version dieser Klasse gespeichert ist, würde nicht versucht werden, einen Namen zu lesen. Denn die alte Version der Klasse animal besitzt keinen Namen.

Aufgabe

Entwickeln Sie ein Programm, das ein Objekt vom Typ std::runtime_error in einer Datei serialisiert und von dieser wieder lädt.