Lambdas

Zum Einstieg eine kurze Wiederholung zu Funktionsobjekten (functor). Ein Funktionsobjekt ist ein Objekt, dessen Klasse den Operator () definiert. Im Beispiel begrenzt der überladene Operator () der Klasse LimitRange das übergebene Datum auf den per Konstruktor festgelegten Bereich. Der Aufruf des überladenen Operators erfolgt in Zeile 37.

#include <print>
#include <cstdlib>

// functor zur Bereichspruefung
class LimitRange
{
    int low = 3;    // untere Grenze
    int high = 12;  // obere Grenze
public:
    LimitRange(int _low, int _high):
        low(_low), high(_high)
    {}
    // Daten auf Bereich begrenzen
    void operator()(int& toCmp)
    {
        if (toCmp<low)
            toCmp = low;
        else
            if (toCmp>high)
                toCmp = high;
    }
};
int main()
{
    // Datenfeld definieren/initialisieren
    int data[6];
    for (auto& elem : data)
       elem = rand()%20;
    // Daten ausgeben
    std::print("Ausgangsdaten: ");
    for (auto elem: data)
        std::print("{},",elem);
    // CheckRange-Objekt definieren
    LimitRange toCheck(3,12);
    // Daten ausserhalb [3...12] ueberschreiben
    for (auto& elem : data)
        toCheck(elem);
    // Daten ausgeben
    std::print("\nDaten begrenzt: ");
    for (auto elem: data)
        std::print("{},",elem);
    std::println();
}

Ausgangsdaten: 1,7,14,0,9,4,
Daten begrenzt: 3,7,12,3,9,4,

Um die Anwendung von Lambda-Ausdrücken sinnvoll demonstrieren zu können, sehen wir uns vorab einen Algorithmus der Standardbibliothek an, der in der Header-Datei algorithm definiert ist.

std::ranges::for_each (data, func);

for_each() ruft für jedes Element im Datenbereich data die Funktion oder das Funktionsobjekt func auf. Somit können die Anweisungen in Zeile 36 und 37 in einer Anweisung zusammengefasst werden.

std::ranges::for_each(data,toCheck);

Lambda-Ausdruck

Ein Lambda-Ausdruck ist ein anonymes (constexpr) Funktionsobjekt und hat Form:

[CAPTURE] <TEMPL> (PARM) -> RTYP {ANWEISUNGEN};

Bis auf die eckigen und geschweiften Klammern sind alle anderen Angaben optional.

Der Vorteil eines Lambda-Ausdrucks gegenüber dem überladenen Operator () ist, dass zum einen keine Klasse definiert muss (dies erledigt der Compiler im Hintergrund) und zum anderen kann der Lambda-Ausdruck dort definiert werden, wo er benötigt wird.

Schreiben wir das vorherige Beispiel so um, dass anstelle der Klasse LimitRange ein Lambda-Ausdruck verwendet wird.

#include <print>
#include <algorithm>

int main()
{
    // Datenfeld definieren/initialisieren
    int data[6];
    for (auto& elem : data)
       elem = rand()%20;
    // Daten ausgeben
    std::print("Ausgangsdaten: ");
    for (auto elem: data)
        std::print("{},",elem);
        
    // Werte auf Bereich 3..12 begrenzen
    std::ranges::for_each(data,
      [](int& elem)  // ** Beginn Lambda **
      {
          if (elem<3)
            elem = 3;
          else
            if (elem>12)
                elem = 12;
          // oder in einer Zeile (Bedingungsoperator)
          //  elem = (elem<3)? 3 : (elem>12)? 12:elem;
      }   // ** Ende Lambda **
      );
   
    // Daten ausgeben
    std::print("\nDaten begrenzt: ");
    for (auto elem: data)
        std::print("{},",elem);
    std::println();
}

Ausgangsdaten: 1,7,14,0,9,4,
Daten begrenzt: 3,7,12,3,9,4,

Der Lambda-Ausdruck erhält im Parameter elem nacheinander alle Elemente des Bereichs data per Referenz übergeben und führt die Bereichsüberprüfung durch. Allerdings hat dieser Lambda-Ausdruck noch den Nachteil, dass die Bereichsgrenzen fest definiert sind.

Datenbindung (capture)

Ein weiterer Vorteil des Lambda-Ausdrucks gegenüber einem überladenen Operator () ist, dass der Lambda-Ausdruck auf Daten seiner Umgebung zugreifen kann. Dazu wird innerhalb der eckigen Klammer eine Datenbindung (capture) angegeben. Sie wird durch folgende Symbole gesteuert:

Bindung Bedeutung
[] Keine Datenbindung
[=] Alle definierten Daten werden per Wert eingebunden
[&] Alle definierten Daten werden per Referenz eingebunden
[&var] var wird per Referenz eingebunden
[=,&var] Alle definierten Daten werden per Wert eingebunden, aber var per Referenz
[this] Bindet das aktuelle Objekt per Referenz ein
[*this] Bindet das aktuelle Objekt als Kopie ein
&list... Bindet ein Parameter-Pack per Referenz ein
list... Bindet ein Parameter-Pack als Kopie ein

Anstatt die untere und obere Grenze fest im Lambda-Ausdruck vorzugeben, können die Grenzen über zwei Daten festgelegt werden. Da die Grenzwerte im Lambda-Ausdruck nicht verändert werden, werden die Daten mit den Grenzwerten per Wert eingebunden. Soll auch noch die Summe der auszuwertenden Daten berechnet werden, wird eine weitere Variable definiert, die per Referenz eingebunden wird da sie im Lambda-Ausdruck verändert wird.

#include <print>
#include <algorithm>

int main()
{
    // Datenfeld definieren/initialisieren
    int data[6];
    for (auto& elem : data)
       elem = rand()%20;
    // Daten ausgeben
    std::print("Ausgangsdaten: ");
    for (auto elem: data)
        std::print("{},",elem);

    const int low = 3;      // unterer Grenzwert
    const int high = 12;    // oberer Grenzwert
    int sum = 0;            // Summe der Daten
    // Werte auf Bereich 3..12 begrenzen
    // und Summe der Werte berechnen
    std::ranges::for_each(data,
        [=,&sum](int& elem)  // ** Beginn Lambda **
        {
          elem = (elem<low)? low : (elem>high)? high:elem;
          sum += elem;
        }   // ** Ende Lambda **
        );

    // Daten ausgeben
    std::print("\nDaten begrenzt: ");
    for (auto elem: data)
        std::print("{},",elem);
    std::println("\nSumme der Daten: {}",sum);
}

Ausgangsdaten:
1,7,14,0,9,4,
Daten begrenzt: 3,7,12,3,9,4,
Summe der Daten: 38

Außer der Datenbindung können in der eckigen Klammer initialisierte Daten aufgeführt werden. Dabei darf für das Datum kein Datentyp angegeben werden und das Datum ist standardmäßig konstant.

#include <print>
#include <algorithm>

int main()
{
    // Datenfeld mit 99 initialisieren
    int data[6];
    std::ranges::for_each(data,
             [start=99](int& elem)
             { elem = start; }
          );
    std::print("Ausgangsdaten: ");
    for (auto elem: data)
        std::print("{},",elem);
}

Ausgangsdaten: 99,99,99,99,99,99,

Soll das initialisierte Datum innerhalb des Lambda-Ausdrucks verändert werden, ist der Lambda-Ausdruck als mutable zu spezifizieren.

#include <print>
#include <algorithm>

int main()
{
    // Datenfeld definieren/initialisieren
    int data[6];
    std::ranges::for_each(data,
             [start=1](int& elem) mutable
             { elem = start++; }
          );
    // Daten ausgeben
    std::print("Ausgangsdaten: ");
    for (auto elem: data)
        std::print("{},",elem);
}

Ausgangsdaten: 1,2,3,4,5,6,

Benannte Lambdas und Rückgabewerte

Wird ein Lambda-Ausdruck an mehreren Stellen benötigt, kann der Lambda-Ausdruck einer 'Lambda-Variable' zugewiesen werden. Der Datentyp der 'Lambda-Variable' muss vom Typ auto sein. Die Ausführung des Lambdas erfolgt durch Angabe der 'Lambda-Variable'.

#include <print>
#include <algorithm>

// Lambda fuer Datenausgabe
auto PrintData = [](int elem)
{
    std::print("{:2},",elem);
};

int main()
{
    // Datenfeld definieren/initialisieren
    int data[] {1,2,3,4,5,6,7};
    // Daten ausgeben
    std::print("Ausgangsdaten: ");
    std::ranges::for_each(data,PrintData);
}

Ausgangsdaten: 1, 2, 3, 4, 5, 6, 7,

Ein solchermaßen definierter Lambda-Ausdruck kann nicht nur Daten erhalten, sondern auch ein Datum mittels einer return-Anweisung zurückliefern. Der Returntyp wird dabei automatisch aus dem zurückgegebenen Datum bestimmt. Soll der Returntyp explizit definiert werden, ist nach der Parameterklammer des Lambda-Ausdrucks ein Pfeil –> und der Datentyp anzugeben.

#include <print>
#include <cstdlib>
#include <algorithm>

// Impliziter Returntyp
auto check = [toCmp=7](int val) {return val<toCmp;};
// Expliziter Returntyp
auto check1 = [toCmp=7](int val) ->bool {return val<toCmp;};

int main()
{
    int data[5];
    for (auto& elem: data)
        elem = std::rand()%20;
    for (auto elem: data)
        std::println("{:2}<7: {}",elem,check(elem));

}

 1<7: true
 7<7: false
14<7: false
 0<7: true
 9<7: false

Lambda-Templates

Erhielten bisher die Lambda-Ausdrücke Parameter mit einem definierten Datentyp, kann durch Angabe von auto als Parameter-Datentyp ein generischer Lambda-Ausdruck definiert werden.

#include <print>
#include <utility>

// Generischer Lambda-Ausdruck
// Vertauscht die Inhalte von val1 und val2
auto swap = [](auto& val1, auto& val2)
    {
        auto temp = std::move(val1);
        val1 = std::move(val2);
        val2 = std::move(temp);
    };

int main()
{
    // int-Datenfeld anlegen
    int data[6]{ 1,2,3,4,5,6 };
    constexpr int SIZE = sizeof data / sizeof data[0];
    // Werte im int-Datenfeld tauschen
    for (auto index = 0; index < SIZE - 1; index++)
        swap(data[index], data[index + 1]);
    std::println("int Getauscht:");
    for (auto index = 0; index < SIZE; index++)
        std::print("{}, ", data[index]);

    // float-Datenfeld anlegen
    float fdata[] = { 1.1f,2.2f,3.3f,4.4f };
    constexpr int FSIZE = sizeof fdata / sizeof fdata[0];
    // Werte im float-Datenfeld tauschen
    for (auto index = 0; index < FSIZE - 1; index++)
        swap(fdata[index], fdata[index + 1]);
    std::println("\nfloat getauscht:");
    for (auto index = 0; index < FSIZE; index++)
        std::print("{}, ", fdata[index]);
}

int Getauscht:
2, 3, 4, 5, 6, 1,
float getauscht:
2.2, 3.3, 4.4, 1.1,

Einen kleinen Fehler hat der Lambda-Ausdruck swap() jedoch noch: Die Datentypen der Argumente beim Aufruf von swap() müssen nicht identisch sein. So ist es z.B. möglich, ein int-Datum mit einem float-Datum zu vertauschen. Um sicherzustellen, dass die Parameter denselben Datentyp besitzen, ist der Lambda-Ausdruck wie folgt als Template zu definieren.

// Lambda-Template
auto swap = []<typename T>(T& val1, T& val2)
{
   auto temp = std::move(val1);
   val1 = std::move(val2);
   val2 = std::move(temp);
}