C++/Tutorial: 14. Funktoren und Funktionszeiger

Aus Scientia
Version vom 6. April 2011, 07:33 Uhr von Alexis Hiemis (Diskussion | Beiträge)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

Weitere Dynamik und Flexibilität wird ins Spiel gebracht, wenn wir zur Laufzeit entscheiden können welche Funktion aufgerufen werden soll. Dafür gibt es mehrere Möglichkeiten, eine wäre zum Beispiel für die Funktion eine abstrakte Klasse mit genau einer Methode zu erstellen und die konkreten möglichen Funktionen dann in Klassen zu stecken, die davon erben, sprich Laufzeitpolymorphie zu nutzen. In manch Sprachen, wie zum Beispiel Java ist das der richtige Weg, C++ bietet uns jedoch noch andere Möglichkeiten, die unter Umständen weniger Bloatcode erzeugen.
Dieses Verfahren wird zum Beispiel für sogenannte Callbackfunktionen verwendet, zum Beispiel bei der Programmierung einer grafischen Oberfläche. Wir können dem Bibliothek dann eine Funktion übergeben, die aufgerufen werden soll, wenn ein Mausklick behandelt werden soll o.ä. Auch sind Funktionen, die Funktionen nehmen eine wunderbare Möglichkeit der Abstraktion und finden starke Verwendung in den Algorithmen, die die Standardbibliothek für Container zur Verfügung stellt, darauf werden wir in einem späteren Kapitel noch einmal einen genaueren Blick werfen.

Funktionszeiger

Das erste was uns C++ bietet, wenn wir dynamisch Funktionen aufrufen wollen sind sogenannte Funktionszeiger. Es handelt sich hierbei um ein relatives low-level Konstrukt, das auch schon in C vorhanden war. Ein Zeiger, der auf eine bereits existierende Funktion zeigt. Haben wir 2 Funktionen foo1 und foo2 mit den selben Parametern und dem selben Rückgabetyp, kann ein Funktionszeiger mit dem entsprechenden Typ auf beide dieser Funktionen zeigen und die Funktionen können dann über den Funktionszeiger aufgerufen werden.
Variablen von Funktionszeigern werden etwas anders deklariert, als normale Variablen. Der Typ eines Funktionszeigers ergibt sich aus Rückgabetyp und Typen der Parameter, ein Funktionszeiger, der auf Funktionen zeigen kann, die nichts zurückgeben und keine Parameter nehmen hätte den Typ void (*)(). Ein Funktionszeiger, der hingegen auf Funktionen zeigt, die 2 ints nehmen und ein int zurückgeben hätte den Typ int(*)(int, int).
Interessant ist nun die Art, wie man einen Funktionspointer deklariert, denn der Bezeichner steht nicht wie bei anderen Namen dahinter, sondern versteckt sich hinter dem *, sodass eine Variablendeklaration ohne Initialisierung so aussieht:

void (*some_function_pointer)(int); //kann auf Funktionen zeigen, die ein int nehmen und nichts zurück geben

Okay, was müssen wir nun tun um an die Addresse einer Funktion zu kommen? Ganz einfach: den Addressoperator verwenden, und zwar vor dem Namen der Funktion: wenn foo eine Funktion ist, gibt &foo die Addresse dieser Funktion zurück. Zusätzlich gibt es syntaktischen Zucker, eine kürzere Schreibweise, denn foo alleine zu schreiben hat bereits die selbe Wirkung wie &foo.
Also weisen wir diese Addresse einem Funktionszeiger zu und können dadurch über den Funktionszeiger die Funktion aufrufen.
Dabei gibt es ebenso wie beim Erlangen der Addresse zwei Möglichkeiten: zum einen können wir some_function_pointer wie einen Zeiger verwenden und die Funktion mit (*some_function_pointer)(0) aufrufen, zum anderen gibt es syntaktischen Zucker, der das ganze verschönert, wir können some_function_pointer beim Aufruf auch als normale Funktion betrachten und sie mit some_function_pointer(0) aufrufen.
Nun ein kleines Beispiel:

void foo1() 
  {
  cout << "hi" << endl;
  }
 
void foo2()
  {
  cout << "ho" << endl;
  }
 
int main()
  {
  void (*function_ptr)()(&foo1); //wir initialisieren den Funktionszeiger mit foo1
  function_ptr(); //hier wird foo1 aufgerufen
  function_ptr = foo2; //auf die & können wir auch verzichten
  function_ptr(); //hier wird foo2 aufgerufen
  }

An Funktionen können diese natürlich auch übergeben wollen, erst dann spielen Funktionszeiger ihre eigentliche Stärke aus.

`Pointer to Member function`

Die bisher behandelte Form von Funktionszeigern ist nicht geeignet um auf Memberfunktionen zu zeigen. Dafür bietet C++ eine besondere Form der Funktionszeiger.
Neben Rückgabetyp und Typ des Parameters gehört zum Typ dieser Funktionszeiger auch noch die Klasse selbst, eine Funktionszeiger kann nur auf Funktionen einer Klasse zeigen(durch Templates, die wir später behandeln werden, ist das nicht wirklich eine Einschränkung), ein Funktionszeiger, der auf Memberfunktionen der Klasse bar zeigen kann, nichts zurückgibt und keine Parameter nimmt hat daher den Typ void(bar::*)().
Initialisierung verhält sich noch recht ähnlich, wir kommen an die Addresse einer Funktion foo1 der Klasse bar mit &bar::foo1.
Bisher haben wir allerdings noch nicht gesagt auf welches Objekt die Funktion aufgerufen werden soll, das geschieht auch erst beim Aufruf.
Sei b eine Instanz von bar und fooptr ein Funktionszeiger, der auf eine Memberfunktion von bar zeigen kann, die keine Parameter nimmt und nichts zurückgibt, dann rufen wir sie in der Form (b.*fooptr)() auf. Syntaktischen Zucker, der den Aufruf und die Initialisierung verschönert, wie es bei normalen Funktionszeigern vorhanden ist gibt es bei dieser Art von Funktionszeigern nicht. Schreiben wir abschließend unser obiges Beispiel einmal leicht um:

class bar
  {
  public:
    bar(int example_member);
    void foo1();
    void foo2();
  private:
    int m_example_member;
  };
 
bar::bar(int example_member) : m_example_member(example_member)
  {}
 
void bar::foo1()
  {
  cout << "hi, example_member:" << m_example_member << endl;
  }
 
void bar::foo2()
  {
  cout << "ho, example_member:" << m_example_member << endl;
  }
 
 
int main()
  {
  bar some_bar(4); //Die Instanz bars auf die die Funktionen aufgerufen werden sollen
  void (bar::*some_function_ptr)()(&bar::foo1); //wir initialisieren den Funktionszeiger mit bar::foo1
  (some_bar.*some_function_ptr)(); //hier wird bar::foo1 aufgerufen, syntaktischen Zucker gibt es hier nicht
  some_function_ptr = &bar::foo2; //auf die & können wir bei diesen Zeigern nicht verzichten
  (some_bar.*some_function_ptr)(); //hier wird bar::foo2 aufgerufen
  }

Pointer to memberfunctions sind zwar nicht so schön, wie es normale Funktionszeiger sind, bieten aber alles nötige, wie wir später in diesem Kapitel noch sehen werden. Direkten Kontakt mit ihnen kann man vermeiden.

Funktoren

Kommen wir zurück zu unserem ersten Ansatz: anstelle eines Zeigers, der auf eine Funktion zeigt, nehmen wir Objekte, mit genau einer Funktion. Das könnte man z.B. mit Laufzeitpolymorphie machen, die einzelnen Objekte haben alle eine call Methode und erben von einer abstrakten Klasse mit dieser call-Methode. C++ gibt uns eine bessere Variante als eine normale Funktion: die Überladung des () Operators. Der () Operator nimmt beliebig viele Parameter und erlaubt es Objekte wie Funktionen aufrufen zu lassen.
Instanzen von Klassen, die den ()-Operator überladen haben nennt man Funktoren.
Gegenüber Funktionszeigern hat das Vor- und Nachteile, wenn eine Funktion bereits existiert ist ein Funktionszeiger natürlich von Vorteil, Funktoren bieten aber größere Flexibilität, durch die Möglichkeit Membervariablen zu haben.
Erstellen wir einmal eine einfache Funktion, die 2 Zahlen miteinander addiert als Funktor:

class add
  {
  public:
    int operator()(int a, int b);
  };
 
int add::operator()(int a, int b)
  {
  return a + b;
  }
 
int main()
  {
  add add_instance;
  cout << add_instance(2,3) << endl; //5
  }

Das Beispiel mag so noch sinnlos erscheinen, aber kann aus mehreren Gründen interessant werden:

  • Wir können add von einer Klasse erben lassen, die diesen Operator überläd, wir können diesen Funktor dann anderen Funktionen übergeben, ohne dass diese wissen, welchen Funktor sie erhalten. Der aktuelle Standard definiert für solche Zwecke die Basisklasse binary_function, mit dem TR1 ist das aber überholt, wir werden das weiter unten weiter besprechen.
  • Wir können add einen Konstruktor verleihen, der den Summanden nimmt, sodass operator() nur noch den zweiten Summanden nimmt. Wir könnten dann im Prinzip Funktionen erzeugen, die zu einem Wert den Wert hinzuaddieren, den wir als Konstruktor angegeben haben.

Letzteren Fall möchte ich einmal an einem Beispiel verdeutlichen:

#include <iostream>
 
using namespace std;
 
class add
  {
  public:
    add(int first_summand);
    int operator()(int second_summand);
  private:
    int m_first_summand;
  };
 
add::add(int first_summand) : m_first_summand(first_summand)
  {}
 
int add::operator()(int second_summand)
  {
  return m_first_summand + second_summand;
  }
 
int main()
  {
  add add2(2); //add2 ist nun wie eine Funktion, die zu ihrem Parameter 2 hinzu addiert
  add add3(3); //add3 ist nun wie eine Funktion, die zu ihrem Parameter 3 hinzu addiert
  cout << add2(4) << endl; //6
  cout << add3(4) << endl; //7
  }

Vereinheitlichung von Funktionszeigern und Funktoren

Wenn wir nun zu unserem Anliegen zurückkommen eine Funktion, wie eine normale Variable zu verwenden, werden wir feststellen, dass wir noch nicht besonders weit gekommen sind. Funktionszeiger sind zwar brauchbar und Pointer-to-memberfunctions sowie Funktoren in gewisser Hinsicht ebenso, dennoch sind sie umständlich und alles in allem fehlt eine Verallgemeinerung. C++ bietet die Mittel diese Konzepte zu verallgemeinern und tut dies auch um uns etwas sehr brauchbares zur Verfügung zu stellen. Der C++ Standard definiert hierfür eine Reihe von Spezialfällen für unäre und binäre Funktionen(Funktionen, die einen oder zwei Parameter nehmen). In der boost-Bibliothek gibt es jedoch eine allgemeinere Form, die auch im TR1 vorhanden ist. Der Typ einer Funktion wird dabei zu std::tr1::function.
std::tr1::function befindet sich im Header tr1/functional und vereinheitlicht Funktionszeiger und Funktoren. Der Typ einer Funktion, die keinen Wert zurückgibt und keine Parameter nimmt wäre function<void ()>, der Typ einer Funktion, die int zurückgibt und 2 ints nimmt, wie es die erste Version unseres add-Funktors macht wäre function<int (int, int)>.
Instanzen von function lassen sich von Funktionszeigern und von Funktoren initialisieren, als Beispiel erstellen wir nun also einmal eine Funktion, die eine Funktion als Parameter nimmt und diese n mal aufruft:

#include <tr1/functional>
 
using namespace std::tr1;
 
void call_repeatedly(function<void ()> const& f, unsigned n)
  {
  for(unsigned i(0); i < n; i++)
    {
    f(); //rufe die übergebene Funktion auf
    }
  }

Erstellen wir nun also eine Funktion, die "hi" ausgibt und lassen diese 5 mal aufrufen:

#include <iostream>
#include <tr1/functional>
 
using namespace std;
using namespace std::tr1;
 
void call_repeatedly(function<void ()> const& f, unsigned n)
  {
  for(unsigned i(0); i < n; i++)
    {
    f(); //rufe die übergebene Funktion auf
    }
  }
 
void say_hi()
  {
  cout << "hi" << endl;
  }
 
int main()
  {
  function<void ()> say_hi_function(&say_hi); //erstelle ein function-Objekt aus einem Funktionszeiger auf say_hi
  call_repeatedly(say_hi_function, 5); //gibt 5 mal "hi" aus
  }

Dem aufmerksamen Leser, wird die Möglichkeit vielleicht schon in den Sinn gekommen sein, aber der Konstruktor von function ist keineswegs explizit deklariert, wir können die main-methode demnach auch viel kürzer schreiben:

int main()
  {
  call_repeatedly(say_hi, 5); //ruft say_hi 5 mal auf
  }

wir könnten say_hi auch als Funktor definieren, wir könnten dort auch die Methode anwenden, die wir bereits bei add angewendet haben um den Text allgemeiner zu halten:

class print_something
  {
  public:
    print_something(std::string const& text);
    void operator()();
  private:
    std::string m_text;
  };
 
print_something::print_something(string const& text) : m_text(text)
  {}
 
void print_something::operator()()
  {
  cout << m_text << endl;
  }
 
int main()
  {
  call_repeatedly(print_something("hi"), 5); //gibt 5 mal "hi" aus
  }

bind

im Header functional befinden sich neben der function Klasse auch noch ein paar Hilfsmethoden, die die Arbeit stark vereinfachen.
Es wäre zum Beispiel einfacher, müssten wir überhaupt keine Hilfsklasse erstellen und in der Tat können wir mit bind bestimmte Parameter von Funktionen binden. Das funktioniert leider nicht mit überladenen Funktionen, wie operator<<, dennoch wäre es wohl einfacher würden wir eine normale Funktion erstellen:

void print_something(string const& text)
  {
  cout << text << endl;
  }

und könnten wir call_repeatedly dann diese Funktion gleich mit einem Parameter aufrufen. Bind ermöglicht dies, bind ist eine funktion, die als ersten Parameter eine Funktion nimmt und als Folgeparameter die Parameter mit der die Funktion aufgerufen werden soll. Als Resultat gibt bind das function-Object mit den gebundenen Parametern zurück, in unserem Fall könnten wir nun z.B. schreiben:

call_repeatedly(bind(print_something, "hi"), 5);

es ist nicht notwendig, dass bind alle Parameter bindet. bind definiert auch Platzhalter für Parameter, die nicht gebunden werden sollen: _1, _2, _3 u.s.w. im namespace std::tr1::placeholders
wollen wir nun also so etwas, wie die add Methode mit bind realisieren, so sähe das folgendermaßen aus:

#include <iostream>
#include <tr1/functional>
 
using namespace std;
using namespace std::tr1;
using namespace std::tr1::placeholders;
 
int add(int a, int b)
  {
  return a + b;
  }
 
int main()
  {
  function<int (int)> add2(bind(add, 2, _1));
  cout << add2(2) << endl; //4
  }

mem_fn

Dennoch fehlt zur vereinheitlichung etwas, denn was ist mit Memberfunktionen?
Im Header tr1/functional befindet sich zu Memberfunktionen eine weitere Hilfsfunktion: mem_fn. Nehmen wir in unserem Beispiel also an, call_repeatedly soll eine Memberfunktion mehrmals aufrufen, dazu müssen wir zunächst mem_fn mit einem Pointer-to-member-function aufrufen, mem_fn macht aus dem Funktionszeiger dann eine Funktion, die als ersten Parameter das Objekt nimmt, auf das die Funktion aufgerufen werden soll und als weitere Parameter die Parameter, die auch die Memberfunktion nimmt.
Nehmen wir als Beispiel also einmal eine Klasse writer, die eine Funktion write besitzt, die einen Text ausgibt, der abhängig von einer Membervariable ist(ähnlich dem was wir beim functor verwendet haben, nur als `normales` Objekt)

class writer
  {
  public:
    writer(std::string const& text);
    void write();
  private:
    std::string m_text;
  };
 
writer::writer(string const& text) : m_text(text)
  {}
 
void writer::write()
  {
  cout << m_text << endl;
  }

Wenn wir nun mem_fn(&writer::write) aufruft ist das Resultat ein function<void (writer*)>. Oftmals wollen wir das ganze dann gleich an ein Objekt binden, was wir mit bind auch problemlos tun können:

writer say_hi("hi");
call_repeatedly(bind(mem_fn(&writer::write), &say_hi), 5); //gibt wieder 5 mal "hi" aus

Da man bind sehr häufig auf Rückgabewerte von mem_fn aufruft, ist bind für Pointer-to-memberfunctions überladen, sodass wir das ganze auch kürzer schreiben können:

writer say_hi("hi");
call_repeatedly(bind(&writer::write, &say_hi), 5); //gibt wieder 5 mal "hi" aus

Überladene Funktionen

Leider können wir so nicht on the fly function Objekte von überladenen Funktionen erstellen, da es sich hier um ein Mittel aus der Standardbibliothek handelt und für C++ keine Möglichkeit besteht aus z.B. &foo zu schlussfolgern, welche Funktion wir meinen. Wohl aber können wir Funktionszeiger von überladenen Funktionen erstellen und daraus dann ein function Objekt erzeugen. Wollen wir also von einer überladenen Funktion foo ein function Objekt auf die Funktion erzeugen, die ein int nimmt und void zurückgibt, müssen wir das wie folgt machen:

function<void (int)> f((void (*) (int)) (&foo));