Die Boost C++ Bibliotheken


Kapitel 3: Funktionsobjekte


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.

Die englische Übersetzung dieses Buchs ist im Buchhandel verfügbar! Es handelt sich dabei um eine aktualisierte Version, die auf den Boost C++ Bibliotheken 1.47.0 vom Juli 2011 basiert. So wurde das Buch im Hinblick auf neue Versionen verschiedener Boost Bibliotheken aktualisiert (zum Beispiel auf Boost.Spirit 2.x, Boost.Signals 2 und Boost.Filesystem 3). Mit insgesamt 38 Boost Bibliotheken werden außerdem mehr Bibliotheken als je zuvor vorgestellt (neu sind zum Beispiel Boost.CircularBuffer, Boost.Intrusive und Boost.MultiArray). Das Buch kann bei Amazon oder anderen Buchhändlern bestellt werden. Zusätzliche Informationen zum Buch finden Sie beim Verlag XML Press.


3.1 Allgemeines

Dieses Kapitel, das sich um Funktionsobjekte dreht, hätte auch den Titel Funktionen höherer Ordnung erhalten können. Damit sind Funktionen gemeint, die als Argument anderen Funktionen übergeben werden oder als Rückgabewert von anderen Funktionen zurückgegeben werden. Funktionen höherer Ordnung sind in C++ als Funktionsobjekte implementiert, so dass der Titel dieses Kapitels dennoch Sinn macht.

Sie lernen in diesem Kapitel mehrere Boost C++ Bibliotheken kennen, die sich um Funktionsobjekte drehen. Während Boost.Bind die beiden aus dem C++ Standard bekannten Funktionen std::bind1st() und std::bind2nd() ersetzt, stellt Boost.Function eine Klasse zur Verfügung, mit der Funktionszeiger verpackt werden können. Abschließend wird Boost.Lambda vorgestellt, mit der sich Funktionen erstellen lassen, die keinen Namen haben.


3.2 Boost.Bind

Boost.Bind ist eine Bibliothek, die etwas vereinfachen und verallgemeinern soll, was mit den beiden Template-Funktionen std::bind1st() und std::bind2nd() aus dem C++ Standard bereits geht: Funktionen mit beliebig vielen Parametern sollen überall dort eingesetzt werden können, wo bisher Funktionen mit einer ganz bestimmten Signatur erwartet wurden. Ein gutes Beispiel sind die vielen Algorithmen, die im C++ Standard definiert sind.

#include <iostream> 
#include <vector> 
#include <algorithm> 

void print(int i) 
{ 
  std::cout << i << std::endl; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), print); 
} 

Der Algorithmus std::for_each() erwartet, dass als dritter Parameter eine Funktion oder ein Funktionsobjekt übergeben wird, die ihrerseits genau einen einzigen Parameter erwarten. Wenn std::for_each() ausgeführt wird, werden die Werte in einem Container - im obigen Beispiel Werte vom Typ int - nacheinander jeweils als einziger Parameter an die Funktion print() übergeben. Soll jedoch eine Funktion aufgerufen werden, deren Signatur nicht mit den Anforderungen eines Algorithmus übereinstimmt, wird es schwierig. Stellen Sie sich zum Beispiel vor, dass anstatt print() die folgende Funktion add() aufgerufen werden soll, um die Summe von einer Zahl aus dem Container und einer Konstanten auf den Bildschirm auszugeben.

void add(int i, int j) 
{ 
  std::cout << i + j << std::endl; 
} 

Weil std::for_each() eine Funktion erwartet, die lediglich einen einzigen Parameter akzeptiert, kann add() nicht direkt an std::for_each() übergeben werden. Stattdessen muss der Quellcode angepasst werden.

#include <iostream> 
#include <vector> 
#include <algorithm> 
#include <functional> 

class add 
  : public std::binary_function<int, int, void> 
{ 
public: 
  void operator()(int i, int j) const 
  { 
    std::cout << i + j << std::endl; 
  } 
}; 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), std::bind1st(add(), 10)); 
} 

Obiges Programm addiert nun zu den Zahlen im Container v jeweils 10 hinzu und gibt dann die Summe auf die Standardausgabe aus. Dies erforderte jedoch größere Änderungen im Quellcode: Die Funktion add() muss in ein Funktionsobjekt umgewandelt werden, das außerdem von std::binary_function abgeleitet werden muss. Mit Boost.Bind wird nun das Zusammenstöpseln von Funktionen einfacher.

Boost.Bind besteht lediglich aus der Template-Funktion boost::bind(), die in der Headerdatei boost/bind.hpp definiert ist. Mit dieser Funktion wird es nun möglich, das Beispiel von eben wie folgt zu implementieren.

#include <boost/bind.hpp> 
#include <iostream> 
#include <vector> 
#include <algorithm> 

void add(int i, int j) 
{ 
  std::cout << i + j << std::endl; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), boost::bind(add, 10, _1)); 
} 

Eine Funktion wie add() muss nun nicht mehr geändert und für std::for_each() in ein Funktionsobjekt umgewandelt werden. Die Funktion kann mit Hilfe von boost::bind() direkt mit std::for_each() verknüpft werden. Dafür muss sie als erster Parameter an boost::bind() übergeben werden.

Da add() zwei Parameter erwartet, müssen zwei zusätzliche Parameter an boost::bind() übergeben werden. Das ist zum einen die Konstante 10, zum anderen wie Sie sehen die etwas merkwürdige Angabe _1.

_1 ist ein sogenannter Platzhalter, der in Boost.Bind definiert ist. Neben _1 sind weitere Platzhalter von _2 bis _9 definiert. Diese Platzhalter verwandeln boost::bind() in eine Funktion, der genauso viele Parameter übergeben werden müssen wie Platzhalter verwendet werden. Ist so wie oben lediglich ein Platzhalter angegeben, wird aus boost::bind() eine unäre Funktion - also eine Funktion, die genau einen einzigen Parameter erwartet. Dies ist hier insofern notwendig, weil std::for_each() als dritten Parameter eine unäre Funktion erwartet.

Wenn Sie das Programm ausführen, ruft std::for_each() eine unäre Funktion auf. Der Wert, der an die unäre Funktion übergeben wird - eine Zahl aus dem Container v - nimmt die Position des Platzhalters _1 ein. Diese Zahl und der Wert 10 werden dann ihrerseits als Parameter an die Funktion add() weitergereicht. Auf diese Weise sieht std::for_each() lediglich eine unäre Funktion, die durch die Art und Weise, wie boost::bind() verwendet wurde, definiert ist. boost::bind() seinerseits ruft einfach eine andere Funktion auf und übergibt Konstanten wie die Zahl 10 oder Werte im Platzhalter als Parameter.

Im Folgenden sehen Sie ein Beispiel, in dem mit Hilfe von boost::bind() eine binäre Funktion definiert wird. Dazu wird nun der Algorithmus std::sort() verwendet, der als dritten Parameter eine binäre Funktion erwartet.

#include <boost/bind.hpp> 
#include <vector> 
#include <algorithm> 

bool compare(int i, int j) 
{ 
  return i > j; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::sort(v.begin(), v.end(), boost::bind(compare, _1, _2)); 
} 

In diesem Beispiel wird deswegen eine binäre Funktion definiert, weil zwei Platzhalter verwendet wurden: _1 und _2. Der Algorithmus std::sort() ruft diese binäre Funktion mit zwei Werten aus dem Container v auf und wertet den Rückgabewert aus, um den Container entsprechend zu sortieren. So, wie die Funktion compare() definiert ist, wird v absteigend sortiert.

Da compare() jedoch bereits eine binäre Funktion ist, hätte in diesem Fall boost::bind() nicht verwendet werden müssen.

#include <boost/bind.hpp> 
#include <vector> 
#include <algorithm> 

bool compare(int i, int j) 
{ 
  return i > j; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::sort(v.begin(), v.end(), compare); 
} 

Die Verwendung von boost::bind() kann trotzdem Sinn machen, wenn zum Beispiel der Container aufsteigend sortiert werden soll, die Definition der Funktion compare() aber nicht geändert werden soll.

#include <boost/bind.hpp> 
#include <vector> 
#include <algorithm> 

bool compare(int i, int j) 
{ 
  return i > j; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::sort(v.begin(), v.end(), boost::bind(compare, _2, _1)); 
} 

In diesem Beispiel ist die Reihenfolge der Platzhalter geändert worden: _2 wird als erster und _1 als zweiter Parameter an compare() übergeben.


3.3 Boost.Ref

Die Bibliothek Boost.Ref wird üblicherweise im Zusammenhang mit Boost.Bind verwendet und soll deswegen im Anschluss an Boost.Bind vorgestellt werden. Sie definiert zwei Funktionen boost::ref() und boost::cref() in der Headerdatei boost/ref.hpp.

Boost.Ref ist dann von Bedeutung, wenn Sie eine Funktion mit boost::bind() verknüpfen wollen, die unter anderem als Parameter eine Referenz erwartet. Weil boost::bind() Parameter kopiert, müssen Referenzen besonders gehandhabt werden.

#include <boost/bind.hpp> 
#include <iostream> 
#include <vector> 
#include <algorithm> 

void add(int i, int j, std::ostream &os) 
{ 
  os << i + j << std::endl; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), boost::bind(add, 10, _1, boost::ref(std::cout))); 
} 

In diesem Beispiel wird auf die bereits bekannte Funktion add() zugegriffen. Diesmal soll jedoch ein Stream als Parameter angegeben werden, auf den die Datenausgabe erfolgen soll. Da Werte an boost::bind() per Kopie übergeben werden, kann std::cout nicht direkt als Parameter angegeben werden. Denn dann würde versucht werden, von std::cout eine Kopie zu erstellen.

Um eine Referenz an std::cout zu übergeben, wird auf boost::ref() zugegriffen. Mit Hilfe dieser Template-Funktion kann ein Stream wie std::cout als Referenz übergeben und das obige Programm kompiliert werden.

Mit der Template-Funktion boost::cref(), die wie oben erwähnt auch von Boost.Ref zur Verfügung gestellt wird, kann eine Referenz auf ein konstantes Objekt übergeben werden.


3.4 Boost.Function

Boost.Function stellt die Klasse boost::function zur Verfügung, um Funktionszeiger zu kapseln. Die Klasse ist in der Headerdatei boost/function.hpp definiert und wird wie folgt verwendet.

#include <boost/function.hpp> 
#include <iostream> 
#include <cstdlib> 
#include <cstring> 

int main() 
{ 
  boost::function<int (const char*)> f = std::atoi; 
  std::cout << f("1609") << std::endl; 
  f = std::strlen; 
  std::cout << f("1609") << std::endl; 
} 

Mit boost::function wird ein Zeiger auf eine Funktion mit einer bestimmten Signatur definiert. Im obigen Programm ist angegeben, dass f auf Funktionen zeigen darf, deren Rückgabewert vom Typ int ist und die einen einzigen Parameter vom Typ const char* erwarten. Einmal definiert können nun Funktionen an f gebunden werden, deren Signatur kompatibel ist. So werden im obigen Programm die Funktionen std::atoi() und std::strlen() an f gebunden.

Beachten Sie, dass keine perfekte Übereinstimmung der Datentypen notwendig ist: Auch wenn std::strlen() so definiert ist, dass diese Funktion einen Rückgabewert vom Typ std::size_t besitzt, kann sie trotzdem an f gebunden werden.

Da f ein Funktionszeiger ist, kann die an f gebundene Funktion über den überladenen Operator operator()() aufgerufen werden. Je nachdem, an welche Funktion f gebunden ist, wird im obigen Beispiel std::atoi() oder std::strlen() aufgerufen.

Wenn f an keine Funktion gebunden ist, wird beim Aufruf die Ausnahme boost::bad_function_call geworfen.

#include <boost/function.hpp> 
#include <iostream> 

int main() 
{ 
  try 
  { 
    boost::function<int (const char*)> f; 
    f(""); 
  } 
  catch (boost::bad_function_call &ex) 
  { 
    std::cout << ex.what() << std::endl; 
  } 
} 

Beachten Sie, dass Sie durch die Zuweisung von 0 einen Funktionszeiger vom Typ boost::function resetten und die Bindung an eine Funktion lösen können. Sollten Sie dann versuchen, eine Funktion aufzurufen, wird ebenfalls eine Ausnahme vom Typ boost::bad_function_call geworfen. Die von boost::function zur Verfügung gestellten Methoden empty() und operator bool() können Sie nutzen um herauszufinden, ob ein Objekt vom Typ boost::function momentan auf eine Funktion verweist oder nicht.

Boost.Function ermöglicht auch, Methoden an Objekte vom Typ boost::function zu binden. Beim Aufruf muss jedoch dann das Objekt angegeben werden, für das die Methode ausgeführt werden soll.

#include <boost/function.hpp> 
#include <iostream> 

struct world 
{ 
  void hello(std::ostream &os) 
  { 
    os << "Hello, world!" << std::endl; 
  } 
}; 

int main() 
{ 
  boost::function<void (world*, std::ostream&)> f = &world::hello; 
  world w; 
  f(&w, boost::ref(std::cout)); 
} 

Wie Sie anhand des obigen Programms sehen, wird als erster Parameter beim Methodenaufruf das Objekt angegeben, für das die an f gebundene Methode ausgeführt werden soll. Das setzt voraus, dass der erste Parameter in runden Klammern bei der Template-Definition ein Zeiger auf die entsprechende Klasse ist. Die nachfolgenden Parameter kennzeichnen dann die Signatur der entsprechenden Methode.

Im obigen Programm wird außerdem wieder die bereits bekannte Funktion boost::ref() aus Boost.Ref verwendet. Wie Sie sehen ist sie auch für Programme von Nutzen, die auf Boost.Function zugreifen, da mit boost::ref() auch hier recht einfach eine Referenz übergeben werden kann.


3.5 Boost.Lambda

Funktionen ohne Namen werden Lambda-Funktionen genannt. Sie existieren in verschiedenen Programmiersprachen, bisher aber nicht in C++. Mit Hilfe der Bibliothek Boost.Lambda können sie nun aber auch in C++-Programmen verwendet werden.

Das Ziel von Lambda-Funktionen ist es, den Code kompakter und damit verständlicher zu machen. Sehen Sie sich zum Beispiel folgendes Programm aus dem ersten Abschnitt dieses Kapitels an.

#include <iostream> 
#include <vector> 
#include <algorithm> 

void print(int i) 
{ 
  std::cout << i << std::endl; 
} 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), print); 
} 

In diesem Programm werden die Zahlen im Container v nacheinander an die Funktion print() übergeben und auf die Standardausgabe ausgegeben. Weil print() nichts anderes tut als einen Parameter vom Typ int auszugeben, sieht die Implementation dieser Funktion sehr einfach aus. Genaugenommen ist sie so einfach, dass es praktisch wäre, wenn die Ausgabe auf std::cout direkt innerhalb des Algorithmus std::for_each() erfolgen könnte und nicht extra eine Funktion print() definiert werden müsste. Das hätte auch den Vorteil, dass der Code nicht mehr räumlich getrennt wäre und die Datenausgabe in einer anderen Funktion erfolgen würde, sondern alles wesentliche innerhalb von std::for_each() angegeben werden könnte. Mit Boost.Lambda ist dies nun tatsächlich möglich.

#include <boost/lambda/lambda.hpp> 
#include <iostream> 
#include <vector> 
#include <algorithm> 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), std::cout << boost::lambda::_1 << "\n"); 
} 

Boost.Lambda stellt verschiedene Konstrukte zur Verfügung, um namenlose Funktionen zu erstellen. Code wird somit dort angegeben, wo er ausgeführt werden soll, ohne ihn extra in eine Funktion packen und diese dann aufrufen zu müssen. So wird im obigen Programm genauso wie im vorherigen angegeben, dass Zahlen im Container v auf die Standardausgabe ausgegeben werden sollen.

Ähnlich wie Boost.Bind definiert auch Boost.Lambda drei Platzhalter namens _1, _2 und _3. Im Gegensatz zu Boost.Bind sind diese Platzhalter jedoch in einem Namensraum definiert. Deswegen wird im obigen Programm über boost::lambda::_1 auf den ersten Platzhalter zugegriffen. Damit sich der C++-Compiler nicht beschwert, muss außerdem die Headerdatei boost/lambda/lambda.hpp eingebunden werden.

Der Vorteil von Boost.Lambda ist nun der, dass Sie wie gewohnt C++-Code schreiben können, auch wenn die Position des Codes - nämlich anstelle des dritten Parameters des Algorithmus std::for_each() - ungewohnt ist. Sie können aber nun tatsächlich den Platzhalter per << an std::cout übergeben, um auf diese Weise die Zahlen im Container v auf die Standardausgabe auszugeben.

Auch wenn Boost.Lambda sehr mächtig erscheint, so funktioniert nicht alles wie gewohnt. So muss im obigen Beispiel für einen Zeilenumbruch anstatt std::endl "\n" angegeben werden, weil der Compiler den Code sonst nicht kompilieren kann. Da der Typ der Lambda-Funktion std::cout << boost::lambda::_1 ein anderer ist als der, den die unäre Template-Funktion std::endl() kennt und erwartet, kann std::endl nicht verwendet werden.

Die nächste Version des C++ Standards wird aller Voraussicht nach die Programmiersprache C++ um Lambda-Funktionen erweitern. Lambda-Funktionen werden dann nicht mit Hilfe einer Bibliothek definiert werden müssen, sondern werden integraler Bestandteil der Programmiersprache C++ sein. Bis zur Veröffentlichung der nächsten Version des Standards und weitreichender Unterstützung der neuen Version durch Compilerhersteller wird es jedoch noch einige Jahren dauern. Bis dahin kann Ihnen Boost.Lambda von Nutzen sein. So sehen Sie im Folgenden ein Programm, das nur Zahlen größer als 1 auf die Standardausgabe ausgibt.

#include <boost/lambda/lambda.hpp> 
#include <boost/lambda/if.hpp> 
#include <iostream> 
#include <vector> 
#include <algorithm> 

int main() 
{ 
  std::vector<int> v; 
  v.push_back(1); 
  v.push_back(3); 
  v.push_back(2); 

  std::for_each(v.begin(), v.end(), 
    boost::lambda::if_then(boost::lambda::_1 > 1, 
    std::cout << boost::lambda::_1 << "\n")); 
} 

In der Headerdatei boost/lambda/if.hpp definiert Boost.Lambda mehrere Konstrukte, um if-Kontrollstrukturen in einer Lambda-Funktion zu verwenden. Das einfachste Konstrukt ist hierbei die Template-Funktion boost::lambda::if_then(), die zwei Parameter erwartet: Im ersten Parameter wird eine Bedingung überprüft. Ist sie wahr, wird der zweite Parameter ausgeführt. Beide Parameter können so wie im obigen Beispiel natürlich auch Lambda-Funktionen sein.

Neben boost::lambda::if_then() stellt Boost.Lambda die Template-Funktionen boost::lambda::if_then_else() und boost::lambda::if_then_else_return() zur Verfügung, die beide drei Parameter erwarten. Darüberhinaus gibt es Template-Funktionen für Schleifen, Cast-Operatoren und sogar für throw, um Ausnahmen in Lambda-Funktionen zu werfen.

Auch wenn mit Hilfe dieser Templates komplexe Lambda-Funktionen in C++ gebildet werden können, stellt sich rasch die Frage nach der Les- und Wartbarkeit. Weil Entwickler außerdem neue Funktionen wie boost::lambda::if_then() erlernen müssen und nicht die aus C++ bekannten Schlüsselwörter if und else einsetzen können, ist ab einer bestimmten Größe der Nutzen von Lambda-Funktionen fraglich. Unter Umständen kann es dann sinnvoller sein, eigenständige Funktionen zu definieren, in denen die aus C++ gewohnten Kontrollstrukturen eingesetzt werden können.


3.6 Aufgaben

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Vereinfachen Sie folgendes Programm, indem Sie das Funktionsobjekt divide_by in eine Funktion umwandeln und die for-Schleife zur Datenausgabe durch einen Algorithmus aus dem C++ Standard ersetzen:

    #include <algorithm> 
    #include <functional> 
    #include <vector> 
    #include <iostream> 
    
    class divide_by 
      : public std::binary_function<int, int, int> 
    { 
    public: 
      int operator()(int n, int div) const 
      { 
        return n / div; 
      } 
    }; 
    
    int main() 
    { 
      std::vector<int> numbers; 
      numbers.push_back(10); 
      numbers.push_back(20); 
      numbers.push_back(30); 
    
      std::transform(numbers.begin(), numbers.end(), numbers.begin(), std::bind2nd(divide_by(), 2)); 
    
      for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) 
        std::cout << *it << std::endl; 
    } 
  2. Vereinfachen Sie folgendes Programm, indem Sie beide for-Schleifen durch Algorithmen aus dem C++ Standard ersetzen:

    #include <string> 
    #include <vector> 
    #include <iostream> 
    
    int main() 
    { 
      std::vector<std::string> strings; 
      strings.push_back("Boost"); 
      strings.push_back("C++"); 
      strings.push_back("Libraries"); 
    
      std::vector<int> sizes; 
    
      for (std::vector<std::string>::iterator it = strings.begin(); it != strings.end(); ++it) 
        sizes.push_back(it->size()); 
    
      for (std::vector<int>::iterator it = sizes.begin(); it != sizes.end(); ++it) 
        std::cout << *it << std::endl; 
    } 
  3. Vereinfachen Sie folgendes Programm, indem Sie die Typdefinition der Variablen processors ändern und die for-Schleife durch einen Algorithmus aus dem C++ Standard ersetzen:

    #include <vector> 
    #include <iostream> 
    #include <cstdlib> 
    #include <cstring> 
    
    int main() 
    { 
      std::vector<int(*)(const char*)> processors; 
      processors.push_back(std::atoi); 
      processors.push_back(reinterpret_cast<int(*)(const char*)>(std::strlen)); 
    
      const char data[] = "1.23"; 
    
      for (std::vector<int(*)(const char*)>::iterator it = processors.begin(); it != processors.end(); ++it) 
        std::cout << (*it)(data) << std::endl; 
    }