C++0x Lambda Funktionen

Lambda Funktionen oder manchmal auch Lambda Expressions genannt, erlauben es an beliebiger Stelle im Code anonyme Funktionen zu definieren. Da Lambda Funktionen anonym sind gibt es natürlich im Gegensatz zu Funktionen keinen Deklaration sondern nur eine Definition:

Definition

lambda-expression:
  lambda-introducer lambda-declarator? compound-statement

Wenn wir von dem optionalen lambda-declarator absehen besteht eine Definition einer Lambda Funktion also aus dem lambda-introducer, der im einfachsten Fall aus einem Paar eckiger Klammern besteht und einem compound-statement, also einem Block bestehend aus Ausdrücken gleich einem Funktionsrumpf. Eine so definierte Lambda Funktion können wir jetzt zum Beispiel direkt an eine Funktion übergeben, oder auch einfach in einer Variablen speichern. Nachdem der genaue Typ des erhaltenen Objektes Compiler abhängig ist können wir den Typ nicht direkt angeben, sondern müssen ihn durch die in C++0x neu eingeführte Möglichkeit den Typ über das Schlüsselwort auto zu bestimmen, feststellen. Das speichern einer der einfachst möglichen Lambda Funktionen in einer Variable kann also zum Beispiel so ausschauen:

auto hello_lambda = []{ std::cout << "Hello Lambda!" << std::endl; };

Die Variable hello_lambda können wir jetzt wie ein normales Funktionsobjekt verwenden, also mit hello_lambda(); aufrufen.

Parameter und Rückgabetyp

Nachdem Lambda Funktionen wie schon im Namen verdeutlicht eine spezielle Variation von Funktionen sind ist es natürlich auch möglich ihnen Parameter zu übergeben bzw. Werte zurück zu geben. An dieser Stelle kommt der bereits erwähnte lambda-declarator ins Spiel. Er erlaubt es unter anderem eine Parameterliste und den Rückgabetyp anzugeben. Parameter werden gleich wie bei Funktionen angegeben und können im wesentlichen auch genau gleich verwendet. Hierbei ist es im Gegensatz zu Funktionen jedoch auch möglich die Funktionsliste weg zu lassen, was einer leeren Parameterliste entspricht. Eine Lambda Funktion welche zwei Zahlen addiert kann also wie folgt ausschauen:

auto lambda_add = [](int lhs, int rhs){ return lhs + rhs; };
 
std::cout << "2 + 4 = " << lambda_add(2, 4) << std::endl;

Bei diesem Beispiel haben wir auch schon einen Rückgabewert verwendet dessen Typ implizit vom Typ int war. Hier gilt die Regel das bei einem Block bestehend nur aus einem einzigen return Statement daraus der Typ des Rückgabewertes bestimmt wird. Hierbei entspricht der Typ dem selben der bei der Anwendung des Operators decltype auf die Argumente des return Statements. In unserem Beispiel also decltype(lhs + rhs). Wir können, bzw. müssen bei mehr als einem Statement im Block den Rückgabetyp aber auch selber angeben. Dies geschieht bei Lambda Funktionen ausschließlich über den sogenannten „trailing return type“, also zu Deutsch, „nachfolgenden Rückgabetyp“. Hierbei folgt der in diesem Fall verpflichtenden Parameterliste -> und anschließend der Rückgabetyp. Dieser muss ein konkreter Typ sein und kann nicht mit Hilfe von auto bestimmt werden. Anhand der folgenden Beispiele können wir verschiedene Möglichkeiten sehen um den Rückgabetyp anzugeben:

// Rückgabetyp ist int
[]() -> int { return 0; };
 
// Rückgabetyp ist float (Addition von int und float ergibt float)
[](int x, float y) -> decltype(x + y){ return x + y; };
 
// Hier gibt es kein return-Statement und auch keine Angabe des
// Rückgabetyps, daher ist dieser hier void
[](int x, int y){ std::cout << "x=" << x << ", y=" << y << std::endl; };

Closure

Eine besondere Eigenschaft von Lambda Funktionen ist die Möglichkeit auf Variablen zuzugreifen die außerhalb der Funktion definiert wurden. Die Gesamtheit aller dieser Variablen wird auch Closure genannt. Natürlich kann man nicht auf beliebige Variablen zugreifen, sondern nur auf solche die gewisse Voraussetzungen erfüllen. Einerseits gilt weiterhin die übliche Namensauflösung von C++, also nur Variablen die in dem Block an der Stelle wo die Lambda Funktion definiert wird bereits definiert sind kommen auch als Kandidaten für eine Variable in einer Closure in Frage. Andererseits müssen man auch angeben auf welche Variablen man zugreifen möchte und auch ob man dies per Wert oder per Referenz machen möchte. Standardmäßig ist kein Zugriff auf externe Variablen möglich. Die gewünschten Variablen können wir durch Erweiterung des lambda-introducer angeben. Hierbei gibt es einerseits die Möglichkeit mittels eines capture-default eine Zugriffsweise als Standard für alle Variablen anzugeben, wobei & für einen Zugriff per Referenz und = für einen Zugriff per Wert steht, oder auch ergänzend oder ausschließlich für bestimmte Variablen die Zugriffsweise angeben, wobei ein & vor dem Variablennamen für einen Zugriff per Referenz und ein Variablenname allein für einen Zugriff per Wert auf die entsprechende Variable steht. Anhand von ein paar Beispielen ist das vermutlich einfacher zu verstehen:

class Test
{
  void test(int param)
  {
    int x;
 
    // Hier ist ein Zugriff auf alle Variablen per Referenz möglich
    [&](){};
 
    // Hier ist der Zugriff auf alle sichtbaren Variablen per Wert möglich
    [=](){};
 
    // Hier können wir auf 'x' per Referenz und auch über den 'this' Zeiger
    // auf die aktuelle Instanz des umschließenden Objektes zugreifen
    [&x, this](){};
 
    // Dieser lambda-introducer führt zu einen Compiler Fehler, da 'this'
    // bereits in '=' inkludiert ist
    [=, this](){};
 
    // Zugriff auf 'x' per Referenz. Auf alle anderen Variablen Zugriff per Wert
    [=, &x](){};
 
    // Zugriff auf 'param' per Wert und auf 'x' per Referenz
    [param, &x](){};
  }
}

Ein wichtige Regel die wir beachten müssen ist das auf Variablen in der Closure die per Wert bearbeitet werden standardmäßig schreiben zugegriffen werden kann. Folgender Code führt also zu einem Fehler:

int x = 42;
[x](){ x *= 2; }; // -> Compiler Fehler

Diese standardmäßige const-ness lässt sich wohl damit erklären, dass mehrere Aufrufe einer Lambda Funktion mit den gleichen Parametern auch das gleiche Ergebnis liefern sollen. Da jede Lambda Funktion nur eine Closure mit einer Instanz jeder referenzierten Variable besitzt würde jeder Aufruf der Lambda Funktion mit einem anderen Wert von x erfolgen obwohl man etwas anderes vermuten würde, da x ja eigentlich per Wert referenziert wird.

Sollten wir den oben gezeigten Code trotzdem kompilieren wollen gibt es die Möglichkeit das mit dem Schlüsselwort mutable zu tun, was den Aufruf der Lambda Funktion welcher normalerweise einem Aufruf eines operator()() const der anonymen Klasse der Lambda Funktion entspricht zu einem Aufruf eines entsprechenden operator()() ohne const macht. Somit ist dann auch der schreibende Zugriff auf per Wert Variablen in der Closure möglich, da diese in Membern der Klasse der Lambda Funktion gespeichert werden:

int x = 42;
 
// so geht es
[x]() mutable { x *= 2; };

:!: Trotzdem sollten wir mutable nur einsetzen wenn es wirklich nötig ist, da es sehr leicht zu unerwünschtem Verhalten und schwer auffindbaren Fehlern führen kann.

Speichern und übergeben an Funktionen

Da wir Variablen mit durch das Schlüsselwort auto bestimmtem Typ nicht als Funktionsparameter verwenden können brauchen wir eine andere Möglichkeit um Lambda Funktionen als Parameter übergeben zu können. Möglich ist das mit Hilfe des Templates std::function aus der neuen Standardbibliothek, welches es erlaubt beliebige Dinge zu speichern welche wie eine Funktion zu verwenden sind. Dazu gehören neben den Verschiedensten Arten von Funktionszeigern unter anderem auch Lambda Funktionen.

Eine Funktion der wir eine Lambda Funktion übergeben können die zwei Integer als Parameter erwartet und einen Integer zurück gibt, können wir somit wie folgt schreiben:

void useLambda(std::function<int(int lhs, int rhs)> compare)
{
  // compare kann wie ein normaler Funktionszeiger verwendet werden
  std::cout << compare(1,2) << std::endl;
}
 
// ...
 
// Funktion mit unterschiedlichen Lambda Funktionen aufrufen
useLambda([](int lhs, int rhs){ return lhs < rhs; });
useLambda([](int lhs, int rhs){ return lhs - rhs; });
useLambda([](int lhs, int rhs){ return lhs > rhs; });

Anwendungsbeispiele

Lambda Funktionen können auf viele Weise eingesetzt werden unter anderem zum Beispiel als Callback Funktionen für verschiedene Events. Sehr viele Möglichkeiten geben sich jedoch erst mit der Verwendung der STL Algorithm Bibliothek:

#include <algorithm>
#include <iostream>
 
void printVector(const std::vector<int>& v)
{
  // Lambda Funktion wird für jedes Element des Vectors aufgerufen
  // und gibt dessen Wert auf die Konsole aus
  std::for_each(v.begin(), v.end(), [](int i){ std::cout << i << " "; });
  std::cout << std::endl;
}
 
int main(int argc, char* argv[])
{
  std::vector<int> v = {50, -10, 20, -30};
 
  // Standardsortierung
  std::sort(v.begin(), v.end());
  printVector(v);
 
  // Jetzt nach Betrag sortiern
  std::sort(v.begin(), v.end(), [](int a, int b){ return abs(a) < abs(b); });
  printVector(v);
 
  // Und noch einen Vector mit "Zufalls"zahlen füllen
  std::vector<int> random_ints(10);
  std::generate(random_ints.begin(), random_ints.end(), [](){ return rand() % 6; });
  printVector(random_ints);
 
  // Die erste gerade Zahl in einem Vector finden
  auto it = std::find_if
  (
    random_ints.begin(),
    random_ints.end(),
    [](int i){ return (i % 2) == 0; }
  );
  if( it != random_ints.end() )
    std::cout << "Die erste gerade Zahl ist " << *it << std::endl;
  else
    std::cout << "Keine gerade Zahl gefunden." << std::endl;
 
  return 0;
}

Quellen