Die Boost C++ Bibliotheken

Kapitel 50. Boost.Fusion

Die Standardbibliothek bietet zahlreiche Container an, die eines gemeinsam haben: Sie sind homogen. In Containern der Standardbibliothek können nur Werte eines Typs gespeichert werden. So können in einem Vektor vom Typ std::vector<int> nur int-Zahlen gespeichert werden, und in einem Vektor vom Typ std::vector<std::string> nur Strings.

Boost.Fusion ermöglicht es, heterogene Container zu erstellen. So können Sie zum Beispiel einen Vektor erstellen, dessen erstes Element eine int-Zahl und dessen zweites Element ein String ist. Boost.Fusion unterstützt aber nicht nur heterogene Container. Die Bibliothek stellt auch Algorithmen zur Verfügung, um diese zu verarbeiten. Stellen Sie sich Boost.Fusion als die Standardbibliothek für heterogene Container vor.

Genaugenommen existiert mit std::tuple seit C++11 auch in der Standardbibliothek ein heterogener Container. Wenn Sie ein Tuple definieren, können Sie unterschiedliche Typen für die Werte angeben, die Sie im Tuple speichern möchten. Boost.Fusion bietet mit boost:fusion::tuple einen ähnlichen Typ an. Während die Standardbibliothek rund um Tuple nicht viel zu bieten hat, sind Tuple für Boost.Fusion lediglich ein Ausgangsbaustein.

Beispiel 50.1. Fusion-Tuple verarbeiten
#include <boost/fusion/tuple.hpp>
#include <string>
#include <iostream>

using namespace boost::fusion;

int main()
{
  typedef tuple<int, std::string, bool, double> tuple_type;
  tuple_type t{10, "Boost", true, 3.14};
  std::cout << get<0>(t) << '\n';
  std::cout << get<1>(t) << '\n';
  std::cout << std::boolalpha << get<2>(t) << '\n';
  std::cout << get<3>(t) << '\n';
}

Im Beispiel 50.1 wird ein Tuple bestehend aus einem int, std::string, bool und double definiert. Das Tuple basiert dabei nicht auf dem aus der Standardbibliothek bekannten Typ std::tuple, sondern auf boost:fusion::tuple. Im nächsten Schritt wird das Tuple instanziiert und mit verschiedenen Werten initialisiert. Abschließend wird mit boost::fusion::get() auf die Elemente des Tuples zugegriffen, um sie auf die Standardausgabe auszugeben. boost::fusion::get() ist gleichbedeutend mit der Funktion std::get(), mit der auf Elemente in einem std::tuple zugegriffen werden kann.

Fusion-Tuple unterscheiden sich nicht von Tuplen aus der Standardbibliothek. So ist es zum Beispiel auch wenig verwunderlich, dass Boost.Fusion eine Funktion boost::fusion::make_tuple() anbietet, die genauso funktioniert wie std::make_tuple(). Folgende Beispiele stellen aufbauend auf Fusion-Tuplen jedoch Möglichkeiten vor, die die Standardbibliothek nicht bietet.

Beispiel 50.2. Mit boost::fusion::for_each() über einen Tuple iterieren
#include <boost/fusion/tuple.hpp>
#include <boost/fusion/algorithm.hpp>
#include <string>
#include <iostream>

using namespace boost::fusion;

struct print
{
  template <typename T>
  void operator()(const T &t) const
  {
    std::cout << std::boolalpha << t << '\n';
  }
};

int main()
{
  typedef tuple<int, std::string, bool, double> tuple_type;
  tuple_type t{10, "Boost", true, 3.14};
  for_each(t, print{});
}

Im Beispiel 50.2 lernen Sie den Algorithmus boost::fusion::for_each() kennen, mit dem über Fusion-Container iteriert werden kann. Die Funktion wird hier eingesetzt, um die Werte im Tuple t nacheinander auf die Standardausgabe auszugeben.

boost::fusion::for_each() ist dem Algorithmus std::for_each() nachempfunden. Während std::for_each() ausschließlich über homogene Container iterieren kann, steht mit boost::fusion::for_each() ein Algorithmus für heterogene Container zur Verfügung.

Beachten Sie, dass Sie keine Iteratoren, sondern einen Container direkt als ersten Parameter an boost::fusion::for_each() übergeben. Möchten Sie nicht über alle Elemente in einem Container iterieren, können Sie mit Hilfe einer View einen Teilausschnitt erstellen.

Beispiel 50.3. Mit boost::fusion::filter_view einen Fusion-Container filtern
#include <boost/fusion/tuple.hpp>
#include <boost/fusion/view.hpp>
#include <boost/fusion/algorithm.hpp>
#include <boost/type_traits.hpp>
#include <boost/mpl/arg.hpp>
#include <string>
#include <iostream>

using namespace boost::fusion;

struct print
{
  template <typename T>
  void operator()(const T &t) const
  {
    std::cout << std::boolalpha << t << '\n';
  }
};

int main()
{
  typedef tuple<int, std::string, bool, double> tuple_type;
  tuple_type t{10, "Boost", true, 3.14};
  filter_view<tuple_type, boost::is_integral<boost::mpl::arg<1>>> v{t};
  for_each(v, print{});
}

Boost.Fusion bietet Views an, die sich wie Container verhalten, im Gegensatz zu Containern jedoch keine Daten speichern. Mit Views kann auf Daten in einem Container anders zugegriffen werden als es der entsprechende Container ermöglicht. Insofern ähneln Views Adaptern aus Boost.Range. Während Adapter jedoch ausschließlich auf einen Container angewandt werden können, können Views auch Daten aus mehreren Containern abbilden.

Im Beispiel 50.3 wird die Klasse boost::fusion::filter_view verwendet, um das Tuple t zu filtern. Es sollen mit boost::fusion::for_each() nicht mehr alle Elemente auf die Standardausgabe geschrieben werden, sondern nur noch die, die auf einem integralen Typ basieren.

boost::fusion::filter_view erwartet als ersten Template-Parameter den Typ des Containers, der gefiltert werden soll. Als zweiter Template-Parameter muss ein Prädikat übergeben werden, das die Elemente filtert. Das Prädikat muss dabei die Elemente basierend auf ihrem Typ filtern.

Boost.Fusion heißt so, weil es zwei Welten vereint: C++-Programme arbeiten mit Werten zur Laufzeit und mit Typen während der Kompilierung. Für Entwickler stehen üblicherweise Werte zur Laufzeit im Vordergrund. Auch sind die meisten Bestandteile der Standardbibliothek darauf ausgerichtet, Werte zur Laufzeit einfacher verarbeiten zu können. Sollen Typen während der Kompilierung verarbeitet werden, kommt die Template Meta-Programmierung ins Spiel. Während zur Laufzeit Werte in Abhängigkeit von anderen Werten verarbeitet werden, werden zur Kompilierung Typen in Abhängigkeit von anderen Typen verarbeitet. Boost.Fusion macht es möglich, Werte in Abhängigkeit von Typen zu verarbeiten.

Der zweite an boost::fusion::filter_view übergebene Template-Parameter ist ein Prädikat, das auf jeden Typ im Tuple angewandt wird. Das Prädikat erhält als Parameter seinerseits einen Typ und muss als Ergebnis true zurückgeben, wenn der entsprechende Typ in der View enthalten sein soll. Wird false zurückgegeben, wird der entsprechende Typ gefiltert.

Im Beispiel 50.3 wird auf die Klasse boost::is_integral aus Boost.TypeTraits zugegriffen. Diesem Template muss als Parameter ein Typ aus dem Tuple übergeben werden, um zu überprüfen, ob er integral ist oder nicht. Da diese Übergabe innerhalb von boost::fusion::filter_view geschieht, wird mit boost::mpl::arg<1> ein Platzhalter aus Boost.MPL verwendet, um eine Lambda-Funktion zu erstellen. Die Aufgabe von boost::mpl::arg<1> ist ähnlich der von boost::phoenix::place_holders::arg1 aus Boost.Phoenix. Das auf diese Weise erstellte Prädikat führt dazu, dass die View v lediglich auf die int- und bool-Elemente des Tuples verweist. Führen Sie Beispiel 50.3 aus, wird Ihnen 10 und true ausgegeben – mehr nicht.

Beispiel 50.4. Iteratorzugriff auf einen Fusion-Container
#include <boost/fusion/tuple.hpp>
#include <boost/fusion/iterator.hpp>
#include <boost/mpl/int.hpp>
#include <string>
#include <iostream>

using namespace boost::fusion;

int main()
{
  typedef tuple<int, std::string, bool, double> tuple_type;
  tuple_type t{10, "Boost", true, 3.14};
  auto it = begin(t);
  std::cout << *it << '\n';
  auto it2 = advance<boost::mpl::int_<2>>(it);
  std::cout << std::boolalpha << *it2 << '\n';
}

Nachdem Sie mit boost::fusion::tuple einen Container und mit boost::fusion::for_each() einen Algorithmus kennengelernt haben, wird es Sie wenig überraschen, im Beispiel 50.4 auf Iteratoren zu treffen. So bietet Boost.Fusion Funktionen wie boost::fusion::begin() und boost::fusion::advance() an. Sie funktionieren ähnlich wie die entsprechenden Funktionen aus der Standardbibliothek.

Beachten Sie, dass die Anzahl der Schritte, um die der Iterator erhöht werden soll, als Template-Parameter an boost::fusion::advance() übergeben werden muss. Mit boost::mpl::int_ wird wieder auf einen Typ aus Boost.MPL zugegriffen.

boost::fusion::advance() gibt einen Iterator mit einem anderen Typ zurück als der, der als Parameter übergeben wurde. Deswegen wird im Beispiel 50.4 ein zweiter Iterator it2 erstellt. Es ist nicht möglich, den Rückgabewert von boost::fusion::advance() dem ersten Iterator it zuzuweisen.

Neben den im Beispiel vorgestellten Funktionen bietet Boost.Fusion weitere an wie boost::fusion::end(), boost::fusion::distance(), boost::fusion::next() oder boost::fusion::prior(), um mit Iteratoren zu arbeiten.

Wenn Sie Beispiel 50.4 ausführen, wird 10 und true ausgegeben.

Beispiel 50.5. Ein heterogener Vektor mit boost::fusion::vector
#include <boost/fusion/container.hpp>
#include <boost/fusion/sequence.hpp>
#include <boost/mpl/int.hpp>
#include <string>
#include <iostream>

using namespace boost::fusion;

int main()
{
  typedef vector<int, std::string, bool, double> vector_type;
  vector_type v{10, "Boost", true, 3.14};
  std::cout << at<boost::mpl::int_<0>>(v) << '\n';

  auto v2 = push_back(v, 'X');
  std::cout << size(v) << '\n';
  std::cout << size(v2) << '\n';
  std::cout << back(v2) << '\n';
}

Bisher haben Sie als einzigen heterogenen Container boost::fusion::tuple kennengelernt. Im Beispiel 50.5 wird mit boost::fusion::vector ein neuer Container vorgestellt.

boost::fusion::vector verhält sich wie ein Vektor. So können Sie über einen Index auf ein Element zugreifen. Das geschieht jedoch nicht über den Operator operator[], sondern über die freistehende Funktion boost::fusion::at(). Der Index wird dabei als Template-Parameter übergeben. Da es sich dabei um einen Typ handeln muss, wird der Index wieder mit boost::mpl::int_ gekapselt.

Dem Vektor soll außerdem ein Element vom Typ char hinzugefügt werden. Dazu wird auf die Funktion boost::fusion::push_back() zugegriffen. Ihr werden zwei Parameter übergeben: Der Vektor, dem ein Element hinzugefügt werden soll, und der entsprechende Wert.

Beachten Sie, dass boost::fusion::push_back() einen neuen Vektor zurückgibt. Der Vektor v wird nicht verändert. Stattdessen entsteht ein neuer Vektor, der eine Kopie des ursprünglichen Vektors ist, aber ein zusätzliches Element enthält.

Das Beispielprogramm gibt mit boost::function::size() die Größe der beiden Vektoren v und v2 aus. Wenn Sie das Programm ausführen, wird Ihnen 4 und 5 ausgegeben. Außerdem wird mit boost::fusion::back() auf das letzte Element in v2 zugegriffen, das ebenfalls ausgegeben wird – ein X.

Wenn Sie sich Beispiel 50.5 näher ansehen, stellen Sie fest, dass es zwischen boost::fusion::tuple und boost::fusion::vector keinen Unterschied gibt. boost::fusion::tuple ist gleichbedeutend mit boost::fusion::vector. Beispiel 50.5 funktioniert auch mit boost::fusion::tuple.

Neben boost::fusion::vector bietet Boost.Fusion mit boost::fusion::deque, boost::fusion::list und boost::fusion::set weitere heterogene Container an. Im folgenden Beispiel wird Ihnen abschließend mit boost::fusion::map ein Container für Schlüssel/Wert-Paare vorgestellt.

Beispiel 50.6. Eine heterogene Map mit boost::fusion::map
#include <boost/fusion/container.hpp>
#include <boost/fusion/sequence.hpp>
#include <boost/fusion/algorithm.hpp>
#include <string>
#include <iostream>

using namespace boost::fusion;

int main()
{
  auto m = make_map<int, std::string, bool, double>("Boost", 10, 3.14, true);
  if (has_key<std::string>(m))
    std::cout << at_key<std::string>(m) << '\n';
  auto m2 = erase_key<std::string>(m);
  auto m3 = push_back(m2, make_pair<float>('X'));
  std::cout << std::boolalpha << has_key<std::string>(m3) << '\n';
}

Im Beispiel 50.6 wird eine heterogene Map mit Hilfe der Funktion boost::fusion::map() erstellt. Die Map hat den Typ boost::fusion::map, der dank des auto-Schlüsselworts nicht explizit angegeben werden muss.

In einer Map vom Typ boost::fusion::map sind wie bei einer std::map Schlüssel/Wert-Paare gespeichert. Die Schlüssel in der Fusion-Map sind jedoch Typen. Das heißt, ein Schlüssel/Wert-Paar besteht aus einem Typ und einem zugeordneten Wert. Dabei darf der Wert auf einem anderen Typ basieren als der Schlüssel. So ist im Beispiel 50.6 dem Schlüssel int der String Boost zugewiesen.

Nachdem die Map erstellt wurde, wird mit boost::fusion::has_key() überprüft, ob ein Schlüssel std::string existiert. Im nächsten Schritt wird mit boost::fusion::at_key() auf das entsprechende Schlüssel/Wert-Paar zugegriffen. Da dem Schlüssel std::string die Zahl 10 zugewiesen ist, wird diese auf die Standardausgabe ausgegeben.

Das Schlüssel/Wert-Paar wird daraufhin mit boost::fusion::erase_key() gelöscht. Beachten Sie, dass sich die Map m nicht ändert. boost::fusion::erase_key() gibt eine neue Map zurück, in der das entsprechende Schlüssel/Wert-Paar fehlt.

Mit boost::fusion::push_back() wird der Map ein neues Schlüssel/Wert-Paar hinzugefügt. Der Schlüssel ist float, der Wert ein X. Für das Schlüssel/Wert-Paar wird auf die Funktion boost::fusion::make_pair() zugegriffen, die vergleichbar ist mit std::make_pair().

Abschließend wird mit boost::fusion::has_key() überprüft, ob die Map einen Schlüssel std::string besitzt. Da dieser gelöscht wurde, wird false zurückgegeben.

Beachten Sie, dass Sie nicht mit boost::fusion::has_key() überprüfen müssen, ob ein Schlüssel existiert, bevor Sie boost::fusion::at_key() verwenden. Wenn Sie boost::fusion::at_key() einen Schlüssel übergeben, der in der entsprechenden Map nicht existiert, erhalten Sie einen Compilerfehler.

Beispiel 50.7. Fusion-Adapter für Strukturen
#include <boost/fusion/adapted.hpp>
#include <boost/fusion/sequence.hpp>
#include <boost/mpl/int.hpp>
#include <iostream>

struct strct
{
  int i;
  double d;
};

BOOST_FUSION_ADAPT_STRUCT(strct,
  (int, i)
  (double, d)
)

using namespace boost::fusion;

int main()
{
  strct s = {10, 3.14};
  std::cout << at<boost::mpl::int_<0>>(s) << '\n';
  std::cout << back(s) << '\n';
}

Boost.Fusion bietet mehrere Makros an, um Strukturen als Fusion-Container verwenden zu können. Dies ist möglich, da eine Struktur als heterogener Container angesehen werden kann. Beispiel 50.7 definiert eine Struktur, die mit Hilfe des Makros BOOST_FUSION_ADAPT_STRUCT zu einem Fusion-Container wird. So ist es möglich, mit Funktionen wie boost::fusion::at() oder boost::fusion::back() auf Elemente in der Struktur zuzugreifen.

Beispiel 50.8. Fusion-Unterstützung für std::pair
#include <boost/fusion/adapted.hpp>
#include <boost/fusion/sequence.hpp>
#include <boost/mpl/int.hpp>
#include <utility>
#include <iostream>

using namespace boost::fusion;

int main()
{
  auto p = std::make_pair(10, 3.14);
  std::cout << at<boost::mpl::int_<0>>(p) << '\n';
  std::cout << back(p) << '\n';
}

Boost.Fusion unterstützt Strukturen wie std::pair oder boost::tuple, ohne Makros verwenden zu müssen. Es muss lediglich die Headerdatei boost/fusion/adapted.hpp eingebunden werden.

Aufgabe

Vervollständigen Sie dieses Programm, so dass debug() die Eigenschaften der im Programm verwendeten Strukturen zu Debugzwecken in die Standardausgabe schreibt:

#include <boost/math/constants/constants.hpp>
#include <iostream>

struct animal
{
    std::string name;
    int legs;
    bool has_tail;
};

struct important_numbers
{
    const float pi = boost::math::constants::pi<float>();
    const double e = boost::math::constants::e<double>();
};

template <class T>
void debug(const T &t)
{
    // TODO: Write member variables of t to standard output.
}

int main()
{
    animal a{ "cat", 4, true };
    debug(a);

    important_numbers in;
    debug(in);
}