C++ Kurs

Funktionsobjekte

Die Themen:

Einleitung
Predicates
Vordefinierte Funktionsobjekte
Funktions-Adapter

Einleitung

Funktionsobjekte (auch functors genannt) haben Sie an verschiedenen Stellen im Kurs schon kennen gelernt. Trotzdem hier noch einmal eine Wiederholung.

Funktionsobjekte sind Objekte, deren Klasse den Operator () überlädt. Nachfolgend ist ein Beispiel für einen solchen Functor aufgeführt, welcher den Wertebereich des an ihn übergebenen Parameters begrenzt.


// Functor-Klasse
struct Limit
{
  void operator()(int& val)
  {
    if (val<0)
      val = 0;
    else
      if (val>50)
        val = 50;
  }
};

Sollen zum Beispiel alle Elemente in einem Container durchlaufen werden, so kann hierfür die Bibliotheksfunktion for_each(...) eingesetzt werden. for_each(...) besitzt folgende Deklaration in der Header-Datei <algorithm>:

template <class InputIterator, class Function>
void for_each(InputIterator first, InputIterator last, Function f);

for_each(...) ruft für jedes Containerelement im Bereich [first...last) die Funktion oder den Functor f auf. f erhält als Parameter das zu bearbeitende Element.


// Functor-Klasse
struct Limit
{
  void operator()(int& val)
  {
    if (val<0)
      val = 0;
    else
      if (val>50)
        val = 50;
  }
};
// Container definieren
typedef std::vector<int> cType;
cType cont;
// Container mit Elementen füllen
...
// Container-Elemente wertmäßig begrenzen
std::for_each(cont.begin(), cont.end(), Limit());

Da der Functor im obigen Beispiel das Element per Referenz erhält, kann er dessen Wert ändern. Beachten Sie auch, dass innerhalb der for_each(...) Parameterklammer das Funktionsobjekt definiert wird, d.h. Limit() ist der Aufruf des Konstruktor der Functor-Klasse und nicht der Aufruf des Functors! Der überladene Operator () selbst wird intern von for_each(...) aufgerufen.

Das obige Beispiel wäre aber auch mit einer entsprechenden einfachen Funktion realisierbar. Warum also der 'Aufwand' mit einem Functor? Nun, nehmen wir einmal an, dass die Grenzwerte für die Container-Elemente nicht zur Compilezeit bekannt sind sondern sich erst zur Laufzeit ergeben. Da for_each(...) aber nur einen Parameter an die Funktion/Functor übergibt, nämlich das aktuelle Elemente, kann diese Anforderung nicht mehr mit einer Funktion gelöst werden, sondern nur noch über einen Functor. Sehen wir uns an, wie dieses Problem mithilfe des Functors gelöst wird.

Da Functor-Klassen 'normale' Klassen sind, nur eben mit überladenem ()-Operator, können diese Klassen auch einen Konstruktor besitzen. Im Beispiel unten wurde die Klasse um einen Konstruktor erweitert, der als Parameter die Unter- und Obergrenze des erlaubten Bereichs erhält. Da der Functor innerhalb der Parameterklammer von for_each(...) definiert wird, muss diese Definition lediglich entsprechend angepasst werden, so wie dargestellt. Und schon können Sie die Grenzwerte beliebig zur Laufzeit festlegen.


// Functor-Klasse
class Limit
{
  int lower,upper;      // Grenzwerte
 public:
  // ctor
  Limit(int l, int u): lower(l), upper(u)
  {}
  // Functor
  void operator()(int& val)
  {
    if (val<lower)
      val = lower;
    else
      if (val>upper)
        val = upper;
   }
};
// Container definieren wie oben
...
// Container-Elemente begrenzen, Grenzwerte an ctor übergeben
std::for_each(cont.begin(), cont.end(), Limit(10,50));

Predicates

Predicates sind Funktionen oder Functors die einen bool-Wert (oder einen in bool konvertierbaren Wert) zurückliefern. Solche Predicates spielen innerhalb der Algorithmen der Standard-Bibliothek eine wichtige Rolle, da sie in Abhängigkeit von einer Bedingung eine Aktion in den Algorithmen steuern. So entfernt z.B. der Algorithmus remove_if(...) Elemente aus einen Container, die einer bestimmten Bedingung genügen.

Die Bedingung ist als Predicate zu definieren. Liefert das Predicate true zurück, so wird das an das Predicate übergebene Element aus dem Container entfernt. Wenn Sie ein Predicate definieren, sollten Sie aber immer darauf achten, dass dieses teilweise innerhalb der Algorithmen kopiert wird und sich das Verhalten dadurch nicht ändern sollte. Das Predicate Mod im Beispiel liefert dann true zurück, wenn der übergebene Wert val sich ohne Rest durch modValue teilen lässt. Würde dieses Predicate innerhalb des remove_if(...) Algorithmus eingesetzt werden, so würden alle Element, die sich ohne Rest teilen lassen, aus dem zu bearbeitenden Container entfernt werden.


// Functor-Klasse, Predicate
class Mod
{
   int modValue;
 public:
   Mod(int mv): modValue(mv)
   {}
   bool operator()(int val)
   {
     return val % modValue ? false : true;
   }
};

Vordefinierte Funktionsobjekte

Die C++ Standard Bibliothek stellt eine Reihe von vordefinierten Funktionsobjekte zur Verfügung.

Functor  Aktion
negate<DTYP>() - param
plus<DTYP>() param1 + param2
minus<DTYP>() param1 - param2
multiplies<DTYP>() param1 * param2
divides<DTYP>() param1 / param2
modulus<DTYP>() param1 % param2
equal_to<DTYP>() Predicate param1 == param2
not_equal_to<DTYP>() Predicate param1 != param2
less<DTYP>() Predicate param1 <  param2
less_equal<DTYP>() Predicate param1 <= param2
greater<DTYP>() Predicate param1 >  param2
greater_equal<DTYP>() Predicate param1 >= param2
logical_not<DTYP>() Predicate !param
logical_and<DTYP>() Predicate param1 && param2
logical_or<DTYP>() Predicate param1 || param2

Sämtliche vordefinierten Functors liegen im Namensraum std und sind in der Header-Datei functional definiert.

Alle Funktionsobjekte erhalten ihre Parameter als const-Parameter übergeben und liefern das Ergebnis der Aktion zurück. D.h. Sie können z.B. den Functor negate<DTYP> nicht mit for_each(...) verwenden um das Vorzeichen aller Elemente eines Containers umzudrehen, da das Element als Konstante übergeben wird und for_each(...) den Returnwert der aufgerufenen Funktion/Functor nicht auswertet.

Übrigens, der Functor less<DTYP> ist das Standard-Sortierkriterium für sortierte Container. Es sortiert die Elemente in einem Container in aufsteigender Reihenfolgen.

Wollen Sie zum Beispiel ein set mit strings in fallender Reihenfolge sortieren, so können Sie bei der Definition des Sets den Functor greater<string> angeben.


// string-set mit fallender Sortierung
typedef set<std::string, greater<string> > sType;
sType stringSet;

Mehr zur Anwendung dieser vordefinierten Functors bei der Behandlung der Algorithmen und in den nächsten beiden Lektionen.

Funktions-Adapter

Auch die Funktionsadapter gehören zu den vordefinierten Functors. Funktionsadapter ermöglichen die Kombination von Functors mit weiteren Functors, Werten oder speziellen Funktionen.

Folgende Funktions-Adapter stehen zur Verfügung und sind in der Header-Datei functional definiert:

Funktions-Adapter Beschreibung
bind1st(operation,wert) führt operation(wert,param) aus und liefert das Ergebnis zurück
bind2nd(operation,wert) führt operation(param,wert) aus und liefert das Ergebnis zurück
not1(operation) führt !operation(param) aus und liefert das Ergebnis zurück
not2(operation) führt !operation(param1,param2) aus und liefert das Ergebnis zurück

Die Funktions-Adapter bind1st(...) und bind2nd(...) erlauben den Einsatz von Functors mit 2 Parametern in Algorithmen, die ansonsten nur Functors mit einem Parameter zulassen.

Sehen wir uns dies anhand eines Beispiels an. Der Algorithmus count_if(...)

template<class InputIterator, class Predicate>
typename iterator_traits<InputIterator>::difference_type
count_if(InputIterator first, InputIterator last, Predicate pred);

ruft für die Elemente im Bereich [first...last) das Predicate pred auf und zählt, wie oft der Wert true zurückgeliefert wird. Das Predicate pred erhält hierbei das zu untersuchende Element übergeben. D.h. mithilfe dieses Algorithmus können Sie die Anzahl der Elemente bestimmen, die einer bestimmten Bedingung genügen.

Nehmen wir nun einmal an, Sie wollen in einem int-Vektor die Elemente zählen, deren Wert kleiner 7 ist. Da das Predicate nur das aktuelle Element erhält, müssen Sie eine eigene Functor-Klasse, wie nachfolgend dargestellt, definieren. Der Vergleichswert ist im Beispiel fest vorgegeben. Alternativ könnten Sie den Vergleichswert auch zur Programmlaufzeit angeben, indem Sie der Klasse einen entsprechenden Konstruktor mitgeben


// Functor-Klasse mit Predicate
struct Less7
{
  bool operator()(int val)
  {
    return val<7;
  }
};
...
// Anzahl der Elemente mit Wert < 7 im Container cont zählen
result = count_if(cont.begin(),  cont.end(), Less7());

Durch den Einsatz eines Funktions-Adapters können Sie sich aber den zusätzlichen Aufwand einer eigenen Functor-Klasse sparen. Die Standard-Bibliothek enthält, wie bereits erwähnt, die Functor-Klasse less<> um Elemente auf kleiner-als zu vergleichen. Dieser Functor erhält dabei als Parameter die beiden zu vergleichenden Elemente übergeben. Damit unser obiger Vergleich mit diesem Functor durchführbar ist, müssen wir irgendwie den Fix-Wert 7 als zweiten Parameter an less<> übergeben. Und genau dies wird durch den Funktions-Adapter bind2nd(...) erreicht. bind2nd(...) ruft den im ersten Parameter spezifizierten Functor auf und übergibt dem Functor das aktuelle Container-Element im ersten Parameter und den Wert im zweiten Parameter.

Somit kann der Aufruf des count_if(...) Algorithmus wie nebenstehend dargestellt umgeschrieben werden. Der less<> Functor führt damit den Vergleich element<7 durch.


// Anzahl der Elemente mit Wert < 7 im Container cont zählen
result = count_if(cont.begin(), cont.end(), bind2nd(less<int>(),7));

Würde anstelle des bind2nd(...) Adapters der bind1st(...) Adapter verwendet werden, so würde dies zur Vergleichsoperation 7 < element im less<> Functor führen.


// Anzahl der Elemente mit 7 < Wert im Container cont zählen
result = count_if(cont.begin(), cont.end(), bind1nd(less<int>(),7));

Die Adapter not1(...) bzw. not2(...) negieren das Ergebnis des als Parameter übergebenen Functors. Wird dieser Adapter z.B. in Verbindung mit dem obigen bind2nd(...) eingesetzt, so liefert count_if(...) jetzt die Elemente die nicht kleiner 7 sind, also die Elemente, für die element >= 7 gilt.


// Anzahl der Elemente mit Wert >= 7 im Container cont zählen
result = count_if(cont.begin(),cont.end(),  not1(bind2nd(less<int>(),7)));

Außer diesen Funktions-Adaptern stellt die Standard-Bibliothek noch Adapter zur Verfügung, um Memberfunktionen innerhalb von Algorithmen einsetzen zu können:

Adapter Beschreibung
mem_fun_ref(memfct) führt memfct() des Container-Objekts aus
mem_fun(memfct) führt memfct() des Container-Objektzeigers aus

Um den Einsatz dieser beiden Adapter zu demonstrieren, sei die nachfolgend dargestellte Klasse Any vorgegeben. Objekte dieser Klasse sollen nun in einem Container abgelegt werden und dann für jedes dieser Container-Objekte dessen Memberfunktion PrintName() aufgerufen werden. Da alle Objekte im Container durchlaufen werden sollen, wird auch hier der for_each(...) Algorithmus eingesetzt. for_each(...) erlaubt aber für die auszuführende Operation nur die Angabe einer normalen Funktion, eines Functors oder einer statischen Memberfunktion, aber nicht die Angabe einer normalen Memberfunktion. Um nun die Memberfunktion PrintName() innerhalb von for_each(...) einsetzen zu können, wird der Adapter mem_fun_ref(memfct) eingesetzt. for_each(...) selbst ruft dann den Adapter mem_fun_ref(...) mit dem aktuellen Objekt auf und mem_fun_ref(...) dann die entsprechende Memberfunktion des übergebenen Objekts.


class Any
{
  std::string data;
 public:
  Any(const std::string& s): data(s)
  {}
  void PrintName() const
  {
    cout << data << endl;
  }
};
// Objekt-Container
vector<Any> objCont;
...
// Fuer jedes Container-Objekt dessen Memberfunktion PrintName() aufrufen
for_each(objCont.begin(), objCont.end(), mem_fun_ref(&Any::PrintName));

Sind in einem Container keine Objekte sonder Objektzeiger abgelegt, so muss anstelle von mem_fun_ref(...) der Adapter mem_fun(...) verwendet werden.


class Any
{
  ...
};
// Objektzeiger-Container
vector<Any*> ptrCont;
...
// Fuer jedes Container-Objekt dessen Memberfunktion PrintName() aufrufen
for_each(ptrCont.begin(), ptrCont.end(), mem_fun(&Any::PrintName));

Wie bereits erwähnt, lassen sich solche Adapter auch kombinieren.

Soll für Container-Objekte eine Memberfunktion aufgerufen werden, die einen Parameter besitzt, so können Sie z.B. den Adapter mem_fun_ref(...) mit dem Adapter bind2nd(...) kombinieren. Im Beispiel wird die Memberfunktion Print(...), die einen const-char* Parameter besitzt, für jedes Any-Objekt im Container objCont aufgerufen. Der zusätzliche const-char* Parameter der Memberfunktion wird über bind2nd(...) an die Memberfunktion übergeben.


class Any
{
  ...
  void Print(const char* header) const
  {
    cout << header << data << endl;
  }
};
// Objektzeiger-Container
vector<Any> objCont;
...
// Fuer jedes Container-Objekt dessen Memberfunktion Print() aufrufen
for_each(objCont.begin(), objCont.end(), bind2nd(mem_fun_ref(&Any::Print),"Name: "));

So, beenden wir an dieser Stelle die Behandlung von Funktionsobjekten und wenden uns in den nächsten beiden Lektion den Algorithmen zu. Dort werden Sie auch einige der aufgeführten vordefinierten Functors wieder finden.