C++ Kurs

Funktionsobjekte & Lambda-Funktionen

Die Themen:

Funktionsobjekte (auch Functors genannt) wurden an verschiedenen Stellen im Kurs zwar schon verwendet, 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.

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

Um zum Beispiel Elemente in einem Container der Reihe nach zu verarbeiten, 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, wobei das zu verarbeitende Element als Parameter übergeben wird.

PgmHeader// Functor-Klasse
struct Limit
{
  void operator()(int& val)
  {
    if (val<0)
      val = 0;
    else
      if (val>50)
        val = 50;
  }
};
// Container definieren
using cType = std::vector<int>;
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. Innerhalb der for_each(...) Parameterklammer wird dazu das Funktionsobjekt definiert, 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 dies nicht mehr ohne globale Variablen mit einer Funktion gelöst werden, sondern nur noch über einen Functor. Sehen wir uns an, wie dieses Problem mit Hilfe des Functors gelöst wird.

Da Functor-Klassen normale Klassen sind, nur eben mit überladenem ()-Operator, besitzen diese Klassen auch einen Konstruktor. 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 die Grenzwerte zur Laufzeit beliebig einstellt werden.

PgmHeader// 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 einem Container, die einer bestimmten Bedingung genügen. Und diese Bedingung ist als Predicate zu definieren. Liefert das Predicate true zurück, so wird das an das Predicate zur Überprüfung übergebene Element aus dem Container entfernt.

Wenn ein Predicate eingesetzt wird, sollte aber immer darauf geachtet werden, 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 Elemente, die sich ohne Rest teilen lassen, aus dem zu bearbeitenden Container entfernt werden.

PgmHeader// 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 Functors 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 Functors erhalten ihre Parameter als const-Parameter übergeben und liefern das Ergebnis der Auswertung zurück. Daraus folgt, dass z.B. der Functor negate<DTYP> nicht in for_each(...) verwendet werden kann 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. Er ist dafür verantwortlich, dass die Elemente in einem Container in aufsteigender Reihenfolgen sortiert werden.

Soll zum Beispiel ein set mit strings in fallender Reihenfolge sortiert werden, so kann bei der Definition des Sets der Functor greater<string> angegeben werden.

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

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

Funktions-Adapter

Auch die Funktionsadapter gehören zu den vordefinierten Functors. Funktionsadapter ermöglichen die Kombination eines Functors mit einem weiteren Functor, Wert oder Funktion.

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. mit Hilfe dieses Algorithmus kann die Anzahl der Elemente bestimmt werden, die einer bestimmten Bedingung genügen.

Nehmen wir einmal an, es sollen in einem int-Vektor die Elemente gezählt werden, deren Wert kleiner 7 ist. Da das Predicate nur das aktuelle Element erhält, muss hierfür eine eigene Functor-Klasse, wie nachfolgend dargestellt, definiert werden. Der Vergleichswert ist im Beispiel fest vorgegeben. Alternativ kann der Vergleichswert auch zur Programmlaufzeit definiert werden, indem er an einen entsprechenden Konstruktor der Klasse übergeben wird.

PgmHeader// 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());

Der Aufwand für eine eigene Functor-Klasse kann durch den geschickten Einsatz eines Funktions-Adapters entfallen. Die Standard-Bibliothek enthält, wie bereits erwähnt, die Functor-Klasse less<> um Elemente auf kleiner-als zu vergleichen. Dieser Functor erhält als Parameter die beiden zu vergleichenden Elemente übergeben. Damit der obige Vergleich mit diesem Functor durchführbar ist, muss aber der Fix-Wert 7 als zweiter Parameter an less<> übergeben werden. 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 nachfolgend dargestellt umgeschrieben werden. Der less<> Functor führt damit den Vergleich element<7 durch.

PgmHeader// 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.

PgmHeader// 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.

PgmHeader// 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 und dann für jedes dieser Container-Objekte die Memberfunktion PrintName() aufgerufen werden. Da alle Objekte im Container durchlaufen werden sollen, wird auch hier der for_each(...) Algorithmus eingesetzt. for_each(...) erlaubt für die auszuführende Operation nur die Angabe einer normalen Funktion, eines Functors oder einer statischen Memberfunktion, nicht aber 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.

PgmHeaderclass 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 sondern Objektzeiger abgelegt, so muss anstelle von mem_fun_ref(...) der Adapter mem_fun(...) verwendet werden.

PgmHeaderclass 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 kann hierfür z.B. der Adapter mem_fun_ref(...) mit dem Adapter bind2nd(...) kombiniert werden. 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.

PgmHeaderclass 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: "));

Lambda Funktionen

Lambda-Funktionen bieten zunächst nichts wesentlich Neues gegenüber den Functors. D.h. anstelle einer Lambda-Funktion kann in der Regel auch ein Functor verwendet werden. So erzeugt auch die Definition einer Lambda-Funktion zunächst noch keinen Code, genauso, wie die Definition eines Functors noch keinen Code erzeugt. Erst beim Aufruf der Lamba-Funktion wird vom Compiler der Code für die Funktion erzeugt. Die so erzeugte Funktion wird auch als Closure bezeichnet.

Im Prinzip sind Lambda-Funktionen anonyme Funktionen, die in der Regel an der Stelle im Programm definiert werden, an der sie benötigt werden. Zusätzlich zu einem Functor kann eine Lambda-Funktion aber auch auf die sie umgebenden Daten zugreifen.

Sehen wir uns zunächst die allgemeine Definition einer Lambda-Funktion an:

[BIND] (ARGS) ->RTYPE { ANWEISUNGEN }

Die beiden eckigen Klammer leiten eine Lambda-Funktion ein und innerhalb der Klammern steht die Datenbindung BIND, die festlegt, welche Daten aus der Umgebung in der Lambda-Funktion zur Verfügung stehen. Die Datenbindung BIND wird durch folgenden Symbole gesteuert:

Beispiele dazu folgen gleich noch, also keine Panik, auch wenn es am Anfang vielleicht kompliziert aussieht. Ist es aber nicht wirklich.

Im Anschluss an die Datenbindung folgt ein rundes Klammerpaar, in dem die optionalen Parameter ARGS der Funktion stehen, gefolgt vom ebenfalls optionalen Returntyp RTYPE der Funktion. Anschließend folgen die Anweisungen der Funktion, eingeschlossen in einem geschweiften Klammerpaar, genauso wie diese bei normalen Funktionen der Fall ist.

Beginnen wir die Einführung der Lambda-Funktionen mit einem sehr einfachen Beispiel, der Ausgabe der Daten aus einem Vektor. Mit den bisherigen Kenntnissen unter Verwendung eines Functors könnte die Ausgabe wie folgt erfolgen:

PgmHeader// Functor fuer die Ausgabe
struct Print
{
    void operator () (int val)
    {
        cout << val << ',';
    }
};
int main()
{
    // Container definieren
    std::vector<int> cont {11,210,41,-10,340};
    // Ausgabe des Vectors mittels functor
    std::for_each(cont.begin(), cont.end(), Print());
}

Übersichtlicher wäre es jedoch, wenn wir für die Ausgabe der Daten nicht eine Klasse definieren müssten, sondern die Ausgabe direkt in der for_each Anweisung vornehmen könnten. Und genau für solche Zwecke ist die Lambda-Funktion die erste Wahl. Sehen wir uns das obige Beispiel mit einer Lambda-Funktion an.

PgmHeaderint main()
{
    // Container definieren
    std::vector<int> cont {11,210,41,-10,340};
    // Ausgabe des Vectors mittels Lambda-Funktion
    std::for_each(cont.begin(), cont.end(),
                  [](int val){cout << val << ',';}
                  );
}

Das Klammerpaar [] leitet die Lambda-Funktion ein. Da keine Datenbindung aus der Umgebung benötigt wird, bleibt diese Klammer leer. Als nächstes folgt der Funktionsparameter val, der den auszugebenden Wert enthält und direkt daran anschließend der Funktionsrumpf mit der Ausgabe.

Steigern wir das Ganze noch etwas. Im nächsten Schritt sollen die Werte des Vektors begrenzt werden. Anstatt jedoch die untere und obere Grenze fest in der Lambda-Funktion vorzugeben, sollen diese Grenzen in der main() Funktion über zwei entsprechende Variablen vorgegeben werden. In diesem Fall muss die Lambda-Funktion eine entsprechende Datenbindung eingehen. Da die Grenzwerte in der Lambda-Funktion nicht verändert werden, werden die Variablen mit den Grenzwerten per Wert eingebunden.

PgmHeaderint main()
{
    // Container definieren
    std::vector<int> cont {11,210,41,-10,340};
    // Grenzwerte definieren
    int upperLimit = 50;
    int lowerLimit = 0;
    // Werte im Vektor begrenzen
    std::for_each(cont.begin(), cont.end(),
        [=](int& val)
        {
            if (val>upperLimit) val=upperLimit;
            else if (val<lowerLimit) val = lowerLimit;
        }
        );
}

Die Lambda-Funktion enthält nun zwei wesentliche Neuerungen. Zum einen erfolgt mittels [=] eine Datenbindung per Wert und zum anderen erhält die Lambda-Funktion nun eine Referenz auf den Wert im Vektor. Durch die Datenbindung [=] hat die Lambda-Funktion Lesezugriff auf alle in ihrem Gültigkeitsbereich definierten Daten, in unserem Fall also auch auf die definierten Grenzwerte. Außerdem muss der Parameter val hier als Referenz übergeben werden, da der Wert in der Lambda-Funktion verändert werden kann.

Sehen wir uns noch eine Lambda-Funktion an, mit deren Hilfe die Summe der Werte in einem Vektor berechnet werden kann.

PgmHeaderint main()
{
    // Container definieren
    std::vector<int> cont {11,210,41,-10,340};
    // Summe loeschen
    auto sum = 0;
    // Werte im Vektor aufsummieren
    std::for_each(cont.begin(), cont.end(),
                  [&](int val) {sum += val;}
                  );
}

Hier erhält die Lambda-Funktion eine Datenbindung per Referenz [&]. Dies ermöglicht der Lambda-Funktion Schreib- und Lesezugriffe auf alle bis dahin definierten Daten in ihrem Gültigkeitsbereich. In unserem Fall summiert die Lambda-Funktion alle Vektor-Werte in der Variablen sum auf.

Ebenso ist es möglich, generische Lambda-Funktionen zu definieren. Bei einer generischen Lambda-Funktion ist mindestens einer Datentypen der Parameter vom Typ auto. Sie sind im Prinzip das moderne Äquivalent zum Funktionstemplate. Auch hier wird je nach tatsächlichem Datentyp erst beim Aufruf der Lambda-Funktion die entsprechende Funktion instanziiert. Sehen wir uns dies an einem einfachen Beispiel an. Aufgabe ist es, ein bestimmtes Bit eines Datums daraufhin zu überprüfen, ob es gesetzt ist oder nicht.

PgmHeader// Generische Lambda-Funktion zum Testen eines Bits auf 1
auto TestBit = [] (auto datum, auto bit)
{
   return (datum & (1<<bit)) != 0;
};

int main()
{
   // Die zu testenden Daten mit unterschiedlichen Datentypen
   unsigned short wert1 = 0x33;
   unsigned long  wert2 = 0b0000'1001'0001'1100;
   // Testen eines ushort Datum
   bool result = TestBit(wert1,1);
   // Und nun testen eines ulong Datums
   result = TestBit(wert2,7);
}

Je nach Datentyp des übergebenen Parameters datum, wird der Compiler einmal die Funktion TestBit(unsigned short,int) und das andere Mal die Funktion TestBit (unsigned long,int) erzeugen und aufrufen.

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