Die Boost C++ Bibliotheken

Kapitel 39. Boost.Phoenix

Boost.Phoenix ist die wichtigste Boost-Bibliothek für die funktionale Programmierung. Während Bibliotheken wie Boost.Bind oder Boost.Lambda ebenfalls die funktionale Programmierung unterstützen, schließt der Funktionsumfang von Boost.Phoenix diese Bibliotheken ein und geht darüber hinaus.

In der funktionalen Programmierung sind Funktionen Objekte und können wie Objekte verarbeitet werden. So ist es mit Boost.Phoenix möglich, eine Funktion als Ergebnis einer anderen Funktion zurückzugeben. Es ist auch möglich, eine Funktion als Parameter an eine andere Funktion zu übergeben. Da Funktionen Objekte sind, kann zwischen Instanziierung und Ausführung unterschieden werden. Ein Zugriff auf eine Funktion ist nicht gleichbedeutend mit einem Aufruf.

Boost.Phoenix unterstützt die funktionale Programmierung mit Hilfe von Funktionsobjekten: Funktionen sind Objekte, die auf Klassen basieren, die den Operator operator() überladen. So verhalten sich Funktionsobjekte tatsächlich wie andere Objekte in C++. Sie können kopiert und zum Beispiel in einem Container gespeichert werden. Sie verhalten sich aber auch wie Funktionen, da sie aufgerufen werden können.

Die funktionale Programmierung ist in C++ nicht neu. Sie können auch ohne Boost.Phoenix eine Funktion als Parameter an eine andere Funktion übergeben.

Beispiel 39.1. Prädikate als globable Funktion, Lambda-Funktion und Phoenix-Funktion
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

bool is_odd(int i) { return i % 2 == 1; }

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  std::cout << std::count_if(v.begin(), v.end(), is_odd) << '\n';

  auto lambda = [](int i){ return i % 2 == 1; };
  std::cout << std::count_if(v.begin(), v.end(), lambda) << '\n';

  using namespace boost::phoenix::placeholders;
  auto phoenix = arg1 % 2 == 1;
  std::cout << std::count_if(v.begin(), v.end(), phoenix) << '\n';
}

Beispiel 39.1 greift auf den Algorithmus std::count_if() zu, um ungerade Zahlen im Vektor v zu zählen. std::count_if() wird dreimal aufgerufen: Einmal mit einem Prädikat in Form einer freistehenden Funktion, einmal mit einer Lambda-Funktion und einmal mit einer Phoenix-Funktion.

Die Phoenix-Funktion unterscheidet sich von der freistehenden Funktion und der Lambda-Funktion darin, dass sie kein umschließendes Gerüst besitzt. Während in den anderen beiden Fällen ein Funktionskopf mit Parameterliste vorhanden ist, besteht die Phoenix-Funktion quasi ausschließlich aus einer Implementation.

Der entscheidende Bestandteil der Phoenix-Funktion ist boost::phoenix::placeholders::arg1. Es handelt sich hierbei um eine Instanz eines Funktionsobjekts. Sie können auf arg1 ähnlich wie auf std::cout zugreifen: Die Objekte existieren, wenn Sie die entsprechende Headerdatei einbinden.

Mit arg1 definieren Sie eine unäre Funktion. Der Ausdruck arg1 % 2 == 1 führt dazu, dass eine neue Funktion entsteht, die genau einen Parameter erwartet. Die Funktion wird nicht sofort ausgeführt, sondern in phoenix gespeichert. phoenix wird an std::count_if() übergeben, von wo die Funktion für jede Zahl in v aufgerufen wird.

arg1 ist ein Platzhalter für den Wert, der beim Aufruf der Phoenix-Funktion übergeben wird. Da hier ausschließlich arg1 verwendet wird, handelt es sich um eine unäre Funktion. Boost.Phoenix bietet weitere Platzhalter wie boost::phoenix::placeholders::arg2 und boost::phoenix::placeholders::arg3 an. Eine Phoenix-Funkion hat immer genauso viele Parameter wie der Platzhalter mit der höchsten Zahl.

Wenn Sie Beispiel 39.1 ausführen, wird dreimal 3 ausgegeben.

Beispiel 39.2. Phoenix-Funktion vs. Lambda-Funktion
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  auto lambda = [](int i){ return i % 2 == 1; };
  std::cout << std::count_if(v.begin(), v.end(), lambda) << '\n';

  std::vector<long> v2;
  v2.insert(v2.begin(), v.begin(), v.end());

  using namespace boost::phoenix::placeholders;
  auto phoenix = arg1 % 2 == 1;
  std::cout << std::count_if(v.begin(), v.end(), phoenix) << '\n';
  std::cout << std::count_if(v2.begin(), v2.end(), phoenix) << '\n';
}

Beispiel 39.2 hebt einen entscheidenden Unterschied zwischen Phoenix- und Lambda-Funktionen hervor. Phoenix-Funktionen benötigen nicht nur keinen Funktionskopf mit Parameterliste. Ihre Parameter sind außerdem typenlos. So erwartet die Lambda-Funktion lambda einen Parameter vom Typ int. Die Phoenix-Funktion phoenix akzeptiert hingegen jeden beliebigen Typ, solange auf ihn der Modulo-Operator mit einer Zahl angewandt werden kann.

Sie können sich Phoenix-Funktionen als Template-Funktionen vorstellen. So wie Template-Funktionen beliebige Typen akzeptieren, ist eine Phoenix-Funktion ebenfalls nicht an bestimmte Typen gebunden. Das ist der Grund, warum phoenix im Beispiel 39.2 als Prädikat sowohl für den Container v als auch v2 verwendet werden kann, obwohl die Zahlen in diesen Containern unterschiedliche Typen haben. Würden Sie versuchen, lambda mit v2 zu verwenden, gäbe es einen Compiler-Fehler.

Anmerkung

Seit C++14 ist es möglich, generische Lambda-Funktionen zu definieren, die Phoenix-Funktionen in nichts nachstehen. Wird der Parameter i in der Lambda-Funktion im obigen Beispiel mit auto anstelle des Typs int definiert, kann die Lambda-Funktion auf beide Container v und v2 angewandt werden.

Beispiel 39.3. Phoenix-Funktionen als verspätet ausgeführter C++-Code
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  using namespace boost::phoenix::placeholders;
  auto phoenix = arg1 > 2 && arg1 % 2 == 1;
  std::cout << std::count_if(v.begin(), v.end(), phoenix) << '\n';
}

Im Beispiel 39.3 sehen Sie eine Phoenix-Funktion, die als Prädikat für std::count_if() alle ungeraden Zahlen größer als 2 zählt. Dazu wird in der Phoenix-Funktion zweimal auf arg1 zugegriffen: Einmal, um den Platzhalter auf größer als 2 zu vergleichen, einmal, um auf ungerade zu überprüfen. Die beiden Bedingungen werden mit einem && verknüpft.

Sie können sich Phoenix-Funktionen als C++-Code vorstellen, der nicht sofort, sondern verspätet ausgeführt wird. Die Phoenix-Funktion im Beispiel 39.3 sieht wie eine herkömmliche Bedingung aus, in der mehrere logische und arithmetische Operatoren verwendet werden. Die Bedingung wird jedoch nicht sofort ausgeführt, sondern erst dann, wenn auf sie innerhalb von std::count_if() zugegriffen wird. Bei diesem Zugriff handelt es sich um einen herkömmlichen Funktionsaufruf.

Beispiel 39.3 gibt 2 aus.

Beispiel 39.4. Explizite Phoenix-Typen
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  using namespace boost::phoenix;
  using namespace boost::phoenix::placeholders;
  auto phoenix = arg1 > val(2) && arg1 % val(2) == val(1);
  std::cout << std::count_if(v.begin(), v.end(), phoenix) << '\n';
}

Beispiel 39.4 verwendet explizite Typen für alle Operanden in der Phoenix-Funktion. Genaugenommen sehen Sie keine Typen, sondern lediglich die Hilfsfunktion boost::phoenix::val(). Diese Funktion gibt ein Funktionsobjekt zurück, das mit dem Wert initialisiert wird, der an die Funktion übergeben wird. Der konkrete Typ des Funktionsobjekts spielt keine Rolle. Entscheidend ist, dass Boost.Phoenix für verschiedene Typen Operatoren wie >, &&, % und == überlädt. Somit werden Bedingungen nicht sofort überprüft. Stattdessen werden Funktionsobjekte verknüpft, um mächtigere Funktionsobjekte zu erstellen. Je nach Zusammensetzung der Operanden werden diese automatisch als Funktionsobjekte erkannt. Falls nicht, können Sie explizit auf Hilfsfunktionen wie val() zugreifen.

Beispiel 39.5. boost::phoenix::placeholders::arg1 und boost::phoenix::val()
#include <boost/phoenix/phoenix.hpp>
#include <iostream>

int main()
{
  using namespace boost::phoenix::placeholders;
  std::cout << arg1(1, 2, 3, 4, 5) << '\n';

  auto v = boost::phoenix::val(2);
  std::cout << v() << '\n';
}

Beispiel 39.5 verdeutlicht, wie arg1 und val() funktionieren. arg1 ist eine Instanz eines Funktionsobjekts. Sie kann direkt verwendet und wie eine Funktion aufgerufen werden. Sie können beliebig viele Parameter übergeben – arg1 gibt den ersten zurück.

val() ist eine Funktion, um eine Instanz eines Funktionsobjekts zu erstellen. Das entsprechende Funktionsobjekt wird mit einem Wert initialisiert. Wird auf die Instanz wie auf eine Funktion zugegriffen, wird der Wert zurückgegeben.

Wenn Sie Beispiel 39.5 ausführen, wird 1 und 2 ausgegeben.

Beispiel 39.6. Eigene Phoenix-Funktionen erstellen
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

struct is_odd_impl
{
    typedef bool result_type;

    template <typename T>
    bool operator()(T t) const { return t % 2 == 1; }
};

boost::phoenix::function<is_odd_impl> is_odd;

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  using namespace boost::phoenix::placeholders;
  std::cout << std::count_if(v.begin(), v.end(), is_odd(arg1)) << '\n';
}

Im Beispiel 39.6 sehen Sie, wie Sie eine eigene Phoenix-Funktion erstellen. Sie verwenden dazu das Template boost::phoenix::function, dem Sie ein Funktionsobjekt übergeben. Im Beispiel handelt es sich um die Klasse is_odd_impl. Diese Klasse überlädt den Operator operator() derart, dass für eine ungerade Zahl, die als Parameter übergeben wird, true und für eine gerade Zahl false zurückgegeben wird.

Beachten Sie, dass Sie den Typ result_type definieren müssen. Boost.Phoenix greift auf ihn zu, um den Typ des Rückgabewerts des Operators operator() zu ermitteln.

is_odd() ist eine Funktion, die Sie genauso verwenden können wie val(). Beide Funktionen geben ein Funktionsobjekt zurück. Beim Aufruf des Funktionsobjekts werden Parameter, die an die Funktionen übergeben werden, an den Operator operator() weitergereicht. Für Beispiel 39.6 bedeutet dies, dass std::count_if() immer noch ungerade Zahlen zählt.

Beispiel 39.7. Freistehende Funktionen in Phoenix-Funktionen umwandeln
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

bool is_odd_function(int i) { return i % 2 == 1; }

BOOST_PHOENIX_ADAPT_FUNCTION(bool, is_odd, is_odd_function, 1)

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  using namespace boost::phoenix::placeholders;
  std::cout << std::count_if(v.begin(), v.end(), is_odd(arg1)) << '\n';
}

Wenn Sie eine existierende freistehende Funktion in eine Phoenix-Funktion umwandeln möchten, können Sie dies wie im Beispiel 39.7 tun. Sie müssen nicht unbedingt wie im vorherigen Beispiel ein Funktionsobjekt definieren.

Sie verwenden das Makro BOOST_PHOENIX_ADAPT_FUNCTION, um aus einer freistehenden Funktion eine Phoenix-Funktion zu machen. Übergeben Sie dem Makro zuerst den Typ des Rückgabewerts, dann den Namen der zu definierenden Boost.Phoenix-Funktion, dann den Namen der freistehenden Funktion und zuletzt die Zahl der Parameter, die die freistehende Funktion erwartet.

Beispiel 39.8. Phoenix-Funktionen mit boost::phoenix::bind()
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

bool is_odd(int i) { return i % 2 == 1; }

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  using namespace boost::phoenix;
  using namespace boost::phoenix::placeholders;
  std::cout << std::count_if(v.begin(), v.end(), bind(is_odd, arg1)) << '\n';
}

Möchten Sie eine freistehende Funktion als Phoenix-Funktion verwenden, können Sie auch wie im Beispiel 39.8 auf boost::phoenix::bind() zugreifen. boost::phoenix::bind() funktioniert genauso wie std::bind(). Sie übergeben als ersten Parameter den Namen der freistehenden Funktion. Alle weiteren Parameter werden an die freistehende Funktion beim Aufruf weitergereicht.

Tipp

Vermeiden Sie boost::phoenix::bind(). Erstellen Sie Ihre eigenen Phoenix-Funktionen. Dies führt zu lesbarerem Code. Gerade bei komplexen Ausdrücken mit mehreren Verknüpfungen ist es wenig hilfreich, sich zusätzlich mit den Details von boost::phoenix::bind() beschäftigen zu müssen.

Beispiel 39.9. Beliebig komplexe Phoenix-Funktionen
#include <boost/phoenix/phoenix.hpp>
#include <vector>
#include <algorithm>
#include <iostream>

int main()
{
  std::vector<int> v{1, 2, 3, 4, 5};

  using namespace boost::phoenix;
  using namespace boost::phoenix::placeholders;
  int count = 0;
  std::for_each(v.begin(), v.end(), if_(arg1 > 2 && arg1 % 2 == 1)
    [
      ++ref(count)
    ]);
  std::cout << count << '\n';
}

Da Funktionsobjekte beliebig implementiert sein können, bietet Boost.Phoenix einige an, die Schlüsselwörter aus C++ simulieren. So können Sie wie im Beispiel 39.8 die Funktion boost::phoenix::if_() aufrufen, um ein Funktionsobjekt zu erstellen, das sich wie if verhält und eine Bedingung überprüft. Ist die Bedingung wahr, wird Code ausgeführt, der mit operator[] an das Funktionsobjekt übergeben wurde. Dabei muss dieser Code natürlich wieder in Form von Funktionsobjekten vorliegen. So können beliebig komplexe Phoenix-Funktionen erstellt werden.

Im Beispiel 39.8 wird die Variable count für jede ungerade Zahl größer als 2 inkrementiert. Damit der Inkrement-Operator nicht direkt auf count angewandt wird, wird count mit Hilfe der Funktion boost::phoenix::ref() in ein Funktionsobjekt gepackt. Im Gegensatz zu boost::phoenix::val() wird kein Wert in das Funktionsobjekt kopiert. Das von boost::phoenix::ref() zurückgegebene Funktionsobjekt speichert eine Referenz – hier eine Referenz auf count.

Tipp

Verwenden Sie Boost.Phoenix nicht, um komplexe Funktionen zu erstellen. Greifen Sie in diesem Fall auf C++11-Lambda-Funktionen zu. Während Boost.Phoenix der Syntax von C++ sehr nahe kommt, tragen vermeintliche Schlüsselwörter wie if_ oder Code-Blöcke, die aus eckigen Klammern bestehen, nicht unbedingt zur besseren Lesbarkeit bei.