C++ Tutorial

Lambdas

Zum Einstieg, eine kurze Wiederholung zu Funktionsobjekten (functor). Ein Funktionsobjekt ist ein Objekt, dessen Klasse den Operator () definiert. Im Beispiel überschreibt der überladene Operator () der Klasse LimitRange das übergebene Datum mit dem Wert 99, wenn der Wert außerhalb des Bereichs [3...12] liegt. Der Aufruf des überladenen Operators erfolgt in Zeile 25.

1: // functor zur Bereichspruefung
2: class LimitRange
3: {
4:    int low = 3;     // untere Grenze
5:    int high = 12;   // obere Grenze
6: public:
7:    LimitRange()
8:    {}
9:    void operator()(int& toCmp)
10:   {
11:      if ((toCmp < low) || (toCmp > high))
12:         toCmp = 99;
13:   }
14: };
15: int main()
16: {
17:    // Datenfeld definieren/initialisieren
18:    int data[6];
19:    for (auto& elem : data)
20:       elem = rand()%20;
21:    // CheckRange-Objekt definieren
22:    LimitRange toCheck;
23:    // Daten ausserhalb [3...12] markieren
24:    for (auto& elem : data)
25:       toCheck(elem);
26: }

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 24 und 25 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 verwendet 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[] {4,-1, 5, 10, 20, 11};
   // Werte <3 oder >12 mit 99 ueberschreiben
   std::ranges::for_each(data,
              [](int& elem) // ** Beginn Lambda **
              {
                 if ((elem < 3) || (elem > 12))
                    elem = 99;
              } // ** Ende Lambda **
      );
   // Daten ausgeben
   for (auto elem: data)
      std::print("{}, ",elem);}

4, 99, 5, 10, 99, 11,

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 der 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 per Kopie ein
list...
Bindet ein Parameter-Pack per Kopie ein
&list...
Bindet ein Parameter-Pack per Referenz ein

Anstatt die untere und obere Grenze fest im Lambda-Ausdruck vorzugeben, können die Grenzen über zwei Variablen festgelegt werden. Da die Grenzwerte im Lambda-Ausdruck nicht verändert werden, werden die Variablen mit den Grenzwerten per Wert eingebunden. Soll gleichzeitig noch die Summe der 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>
#include <iostream>
int main()
{
   // Datenfeld definieren/initialisieren
   int data[] {4,-1, 5, 10, 20, 11};
   // Daten ausgeben
   std::cout << "Ausgangsdaten: ";
   for (auto elem: data)
      std::print("{},", elem);
   std::cout << '\n';
   int low = 3;     // Unterer Grenzwert
   int high = 12;   // Oberer Grenzwert
   int sum = 0;     // Summe der Daten
   // Werte <low oder >high mit 0 ueberschreiben
   std::ranges::for_each(data,
       [low,high,&sum](int& elem)
       {
          if ((elem < low) || (elem > high))
             elem = 0;
          else
             sum += elem;
       }
   );
   // Daten ausgeben
   std::cout << "Korrigierte Daten: ";
   for (auto elem: data)
      std::print("{},", elem);
   std::println("\nSumme: {}",sum);
}

Ausgangsdaten: 4,-1,5,10,20,11,
Korrigierte Daten: 4,0,5,10,0,11,
Summe: 30

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.

1: int main()
2: {
3:    // Datenfeld definieren
4:    int data[6];
5:    // Datenfeld mit dem Wert 99 initialiseren
6:    std::ranges::for_each(data,
7:        [start = 99](int& elem)
8:        {
9:           elem = start;
10:       }
11:    );
12: }

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

1: int main()
2: {
3:    // Datenfeld definieren
4:    int data[6];
5:    // Datenfeld mit aufsteigenden Werten initialiseren
6:    std::ranges::for_each(data,
7:        [start = 1](int& elem) mutable
8:        {
9:           elem = start++;
10:       }
11:    );
12: }

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'.

1: // Lambda definieren
2: auto print = [](int elem)
3:              { std::print("{:4}, ", elem); };
4:
5: int main()
6: {
7:    // Datenfeld definieren
8:    int data[6];
9:    ...
10:   // data Elemente ausgeben
11:   std::ranges::for_each(data, print);
12:   // myData definieren und ausgeben
13:   int myData[]{ 22,33,44 };
14:   std::ranges::for_each(myData, print);
15: }

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.

1: // Impliziter Returntyp
2: auto check = [toCmp=7] (int val) {return val<toCmp;};
3: // Expliziter Returntyp
4: auto check1 = [toCmp=7] (int val) ->bool {return val<toCmp;};
5:
6: int main()
7: {
8:    ...
9:    for (auto elem : data)
10:      std::println("{}<7: {}",elem,check(elem));
11: }

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.

1: // Generischer Lambda-Ausdruck
2: auto swap = [](auto& val1, auto& val2)
3: {
4:    auto temp = std::move(val1);
5:    val1 = std::move(val2);
6:    val2 = std::move(temp);
7: };
8:
9:
10: int main()
11: {
12:    // int-Datenfeld definieren
13:    int data[6];
14:    constexpr int SIZE = sizeof data / sizeof data[0];
15:    // ... int-Daten bearbeiten
16:    // Werte im int-Datenfeld tauschen
17:    for (auto index = 0; index < SIZE - 1; index++)
18:       swap(data[index], data[index + 1]);
19:    // float-Datenfeld definieren/initialisieren
20:    float fdata[] = { 1.1f,2.2f,3.3f,4.4f };
21:    constexpr int FSIZE = sizeof fdata / sizeof fdata[0];
22:    // Werte im float-Datenfeld tauschen
23:    for (auto index = 0; index < FSIZE - 1; index++)
24:       swap(fdata[index], fdata[index + 1]);
25: }

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 als Template zu definieren. Dies erfolgt durch Angabe von <template T> nach der Datenbindung.

1: // Lambda-Template
2: auto swap = []<typename T>(T& val1, T& val2)
3: {
4:    auto temp = std::move(val1);
5:    val1 = std::move(val2);
6:    val2 = std::move(temp);
7: }

Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz