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);
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.
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:
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: }
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