Die Boost C++ Bibliotheken

Kapitel 66. Boost.Flyweight

Boost.Flyweight ist eine Bibliothek, die es einfach macht, das gleichnamige Entwurfsmuster einzusetzen – auf Deutsch Fliegengewicht genannt. Dabei geht es darum, Speicherplatz zu sparen, wenn sich viele Objekte Daten teilen. Anstatt die gleichen Daten mehrfach in Objekten zu speichern, sorgt das Entwurfsmuster Flyweight dafür, dass die geteilten Daten nur einmal im Speicher gehalten werden und alle Objekte auf diese Daten verweisen. Während man dieses Entwurfsmuster auch selbst zum Beispiel über Zeiger umsetzen könnte, geht dies mit einer Bibliothek wie Boost.Flyweight einfacher.

Beispiel 66.1. Hunderttausend gleiche Strings ohne Boost.Flyweight
#include <string>
#include <vector>

struct person
{
  int id_;
  std::string city_;
};

int main()
{
  std::vector<person> persons;
  for (int i = 0; i < 100000; ++i)
    persons.push_back({i, "Berlin"});
}

Im Beispiel 66.1 werden hunderttausend Objekte vom Typ person erstellt. person besitzt zwei Eigenschaften: Über id_ werden Personen identifiziert, und city_ speichert die Stadt, in der Personen leben. In diesem Beispiel leben alle Personen in Berlin. Deswegen wird die Eigenschaft city_ aller hunderttausend Objekte auf Berlin gesetzt. Im Beispiel werden folglich hunderttausend Strings verwendet, die alle den gleichen Wert speichern. Mit Boost.Flyweight kann anstatt hunderttausend Strings ein String verwendet und der Speicherbedarf drastisch reduziert werden.

Beispiel 66.2. Ein String statt hunderttausend Strings mit Boost.Flyweight
#include <boost/flyweight.hpp>
#include <string>
#include <vector>
#include <utility>

using namespace boost::flyweights;

struct person
{
  int id_;
  flyweight<std::string> city_;
  person(int id, std::string city) : id_{id}, city_{std::move(city)} {}
};

int main()
{
  std::vector<person> persons;
  for (int i = 0; i < 100000; ++i)
    persons.push_back({i, "Berlin"});
}

Um Boost.Flyweight zu verwenden, muss wie im Beispiel 66.2 die Headerdatei boost/flyweight.hpp eingebunden werden. Boost.Flyweight bietet weitere Headerdateien an, die jedoch nur eingebunden werden müssen, wenn detaillierte Einstellungen in der Bibliothek geändert werden sollen.

Alle von Boost.Flyweight definierten Klassen und Funktionen befinden sich im Namensraum boost::flyweights. Im Beispiel 66.2 wird ausschließlich auf die Klasse boost::flyweights::flyweight zugegriffen, die die wichtigste Klasse dieser Bibliothek darstellt. Die Eigenschaft city_ hat nun nicht mehr den Typ std::string, sondern den Typ flyweight<std::string>. Diese Änderung reicht aus, um das Fliegengewicht-Entwurfsmuster einzusetzen und den Speicherbedarf des Programms zu verringern.

Beispiel 66.3. boost::flyweights::flyweight mehrfach verwenden
#include <boost/flyweight.hpp>
#include <string>
#include <vector>
#include <utility>

using namespace boost::flyweights;

struct person
{
  int id_;
  flyweight<std::string> city_;
  flyweight<std::string> country_;
  person(int id, std::string city, std::string country)
    : id_{id}, city_{std::move(city)}, country_{std::move(country)} {}
};

int main()
{
  std::vector<person> persons;
  for (int i = 0; i < 100000; ++i)
    persons.push_back({i, "Berlin", "Germany"});
}

Im Beispiel 66.3 wurde der Klasse person eine zweite Eigenschaft country_ hinzugefügt. Diese Eigenschaft soll speichern, in welchem Land Personen leben. Da alle Personen in Berlin leben, leben sie folglich auch im gleichen Land. Deswegen wird boost::flyweights::flyweight auch zur Definition der Eigenschaft country_ verwendet.

Boost.Flyweight verwendet intern einen Container, in dem Objekte gespeichert werden. Er stellt sicher, dass nicht mehrere Objekte mit gleichen Werten existieren. Boost.Flyweight verwendet dazu standardmäßig einen Hash-Container – also einen Container ähnlich wie std::unordered_set. Für unterschiedliche Typen werden unterschiedliche Hash-Container verwendet. Da im Beispiel 66.3 beide Eigenschaften city_ und country_ Strings sind, wird nur ein Container verwendet. Das ist in diesem Beispiel kein Problem, da der Container nur zwei Strings mit Berlin und Germany speichert. Würden viele unterschiedliche Städte und Länder gespeichert werden müssen, wäre es besser, wenn Städte in einem und Länder in einem anderen Container gespeichert werden würden.

Beispiel 66.4. boost::flyweights::flyweight mit Tags mehrfach verwenden
#include <boost/flyweight.hpp>
#include <string>
#include <vector>
#include <utility>

using namespace boost::flyweights;

struct city {};
struct country {};

struct person
{
  int id_;
  flyweight<std::string, tag<city>> city_;
  flyweight<std::string, tag<country>> country_;
  person(int id, std::string city, std::string country)
    : id_{id}, city_{std::move(city)}, country_{std::move(country)} {}
};

int main()
{
  std::vector<person> persons;
  for (int i = 0; i < 100000; ++i)
    persons.push_back({i, "Berlin", "Germany"});
}

Im Beispiel 66.4 wird ein zweiter Template-Parameter an boost::flyweights::flyweight übergeben. Es handelt sich hierbei um einen Tag. Tags sind beliebige Typen, die ausschließlich dazu dienen, die Typen, auf denen city_ und country_ basieren, unterscheidbar zu machen. So sind im Beispiel 66.4 zwei leere Strukturen city und country definiert worden, die als Tags verwendet werden. Es hätten aber genauso gut zum Beispiel int und bool verwendet werden können.

Die Tags führen dazu, dass city_ und country_ auf unterschiedlichen Typen basieren. Somit werden zwei Hash-Container von Boost.Flyweight verwendet – der eine speichert Städte, der andere Länder.

Beispiel 66.5. Template-Parameter von boost::flyweights::flyweight
#include <boost/flyweight.hpp>
#include <boost/flyweight/set_factory.hpp>
#include <boost/flyweight/no_locking.hpp>
#include <boost/flyweight/no_tracking.hpp>
#include <string>
#include <vector>
#include <utility>

using namespace boost::flyweights;

struct person
{
  int id_;
  flyweight<std::string, set_factory<>, no_locking, no_tracking> city_;
  person(int id, std::string city) : id_{id}, city_{std::move(city)} {}
};

int main()
{
  std::vector<person> persons;
  for (int i = 0; i < 100000; ++i)
    persons.push_back({i, "Berlin"});
}

Außer einem Tag können boost::flyweights::flyweight auch andere Template-Parameter übergeben werden. Im Beispiel 66.5 sind dies boost::flyweights::set_factory, boost::flyweights::no_locking und boost::flyweights::no_tracking. Beachten Sie, dass zur Verwendung dieser Klassen zusätzliche Headerdateien eingebunden werden müssen.

boost::flyweights::set_factory gibt an, dass Boost.Flyweight keinen Hash-Container, sondern einen sortierten Container ähnlich wie std::set verwenden soll. Über boost::flyweights::no_locking wird die Unterstützung für Multithreading deaktiviert, die standardmäßig verwendet wird. boost::flyweights::no_tracking wiederum gibt an, dass Boost.Flyweight nicht erfassen soll, ob Objekte, die in den intern verwendeten Containern gespeichert sind, verwendet werden. Werden Objekte nicht mehr verwendet, erkennt Boost.Flyweight das standardmäßig und kann sie von den intern verwendeten Containern entfernen. Mit boost::flyweights::no_tracking wird dieser Erkennungsmechanismus ausgeschaltet. Das führt zu einer höheren Performance, bedeutet aber auch, dass intern verwendete Container nur größer werden können und nie kleiner.

Boost.Flyweight bietet weitergehende Einstellungsmöglichkeiten an. So kann zum Beispiel im Detail angegeben werden, welcher Container intern verwendet werden soll.

Aufgabe

Verbessern Sie dieses Programm, indem Sie Boost.Flyweight verwenden. Setzen Sie die Hilfsmittel aus Boost.Flyweight mit deaktiviertem Support für Multithreading ein:

#include <string>
#include <vector>
#include <memory>

int main()
{
    std::vector<std::shared_ptr<std::string>> countries;
    auto germany = std::make_shared<std::string>("Germany");
    for (int i = 0; i < 500; ++i)
        countries.push_back(germany);
    auto netherlands = std::make_shared<std::string>("Netherlands");
    for (int i = 0; i < 500; ++i)
        countries.push_back(netherlands);
}