Funktionen, die du dort schreibst, wo du sie benutzt
Auf der vorherigen Seite hast du gesehen, wie Überladung mehreren Funktionen erlaubt, sich einen Namen zu teilen. Aber manchmal willst du gar keine benannte Funktion - du brauchst ein winziges Stück Logik einmal, genau dort, wo du es verwendest, und ihm einen Namen zu geben würde nur Unordnung schaffen. Genau das ist ein Lambda: eine anonyme Funktion, die du inline definieren kannst.
Ein Lambda hat eine unverkennbare vierteilige Form:
[capture](parameters) -> return_type { body }
Die [] sind das verräterische Zeichen, dass du ein Lambda vor dir hast. Der Rückgabetyp ist optional - der Compiler leitet ihn meist ab. Hier das einfachste mögliche:
greet ist einfach eine Variable (ihr Typ ist unaussprechlich, also speicherst du sie mit auto), die du mit () aufrufen kannst. Lambdas mit Parametern funktionieren genau wie normale Funktionen:
Captures: in den umgebenden Gültigkeitsbereich greifen
Der Teil, der Lambdas zu mehr als nur namenlosen Funktionen macht, ist die Capture-Liste - die []. Sie erlaubt dem Lambda, Variablen aus dem Gültigkeitsbereich zu verwenden, in dem es definiert wurde, nicht nur seine eigenen Parameter.
Erfasse per Wert mit [x]: das Lambda erhält seine eigene Kopie, eingefroren im Moment, in dem das Lambda erstellt wird.
Beachte, dass scale(5) 50 ausgegeben hat, indem es den Wert 10 von factor verwendet hat, der existierte, als das Lambda erstellt wurde. Capture per Wert macht eine Momentaufnahme.
Erfasse per Referenz mit [&x]: das Lambda verweist auf die ursprüngliche Variable, sieht spätere Änderungen und kann sie verändern.
Du kannst auch alles, was das Lambda verwendet, mit [=] (alles per Wert) oder [&] (alles per Referenz) erfassen. Sie sind bequem, aber explizit zu sein - [total] oder [&total] - dokumentiert genau, was das Lambda anfasst, und ist leichter nachzuvollziehen.
Die Falle der hängenden Referenz
Capture per Referenz ist mächtig und gefährlich zugleich. Die Referenz ist nur gültig, solange die ursprüngliche Variable lebt. Wenn das Lambda das überlebt, was es erfasst hat, bekommst du eine hängende Referenz und undefiniertes Verhalten - das Programm kann abstürzen, Müll ausgeben oder zufällig zu funktionieren scheinen.
Das ist der klassische Fehler: ein Lambda zurückzugeben, das eine lokale Variable per Referenz erfasst.
auto makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // FEHLER: count stirbt hier
}
// Das zurückgegebene Lambda referenziert jetzt zerstörten Speicher.
Wenn makeCounter zurückkehrt, wird seine lokale Variable count zerstört, aber das Lambda hält weiterhin eine Referenz darauf. Das zurückgegebene Lambda aufzurufen, greift auf toten Speicher zu. Die Lösung ist, per Wert zu erfassen, damit das Lambda seinen eigenen Zustand besitzt:
Faustregel: Erfasse nur dann per Referenz, wenn das Lambda sofort und lokal verwendet wird (wie bei den Algorithmen weiter unten). In dem Moment, in dem ein Lambda gespeichert, zurückgegeben oder später ausgeführt wird, ziehe Capture per Wert vor.
mutable und Rückgabetypen
Hast du das mutable in jenem letzten Beispiel bemerkt? Standardmäßig ist eine Per-Wert-Capture innerhalb des Lambdas const - du kannst die Kopie lesen, aber nicht ändern. mutable hinzuzufügen erlaubt dem Lambda, seine eigenen Kopien zwischen Aufrufen zu verändern.
mutable betrifft nur die private Kopie des Lambdas - das äußere seen bleibt unberührt, was ja gerade der ganze Sinn der Capture per Wert ist.
Meistens leitet der Compiler den Rückgabetyp einwandfrei ab. Du musst ihn nur dann mit -> ausbuchstabieren, wenn es Mehrdeutigkeit gibt, etwa bei einem Lambda, das in verschiedenen Zweigen verschiedene Typen zurückgeben könnte:
// Ohne -> kann der Compiler sich nicht zwischen int und double entscheiden
auto half = [](int n) -> double {
if (n % 2 == 0) return n / 2; // int
return n / 2.0; // double
};
Lambdas und Algorithmen: der eigentliche Gewinn
Der Grund, warum Lambdas zu C++ hinzugefügt wurden, ist, den Algorithmen der Standardbibliothek kurze Logikstücke zuzuführen. Vor Lambdas musstest du eine separate benannte Funktion oder ein umständliches Funktionsobjekt fern von dem Ort schreiben, an dem es verwendet wurde. Jetzt lebt die Logik direkt an der Aufrufstelle.
Das häufigste Beispiel ist eine benutzerdefinierte Sortierreihenfolge:
Captures glänzen hier, weil das Lambda einen Wert hereinziehen kann, um danach zu filtern oder zu zählen. Dies zählt, wie viele Zahlen einen vom Benutzer gewählten Schwellenwert überschreiten:
Da diese Lambdas sofort verwendet werden und die umgebende Funktion nicht überleben, wäre Capture per Referenz ([&passMark]) hier ebenfalls sicher - aber per Wert ist genauso klar und hängt nie.
Als Nächstes: Zeiger
Lambdas haben leise eine tiefergehende Frage aufgeworfen: Wenn du [&x] erfasst, hält sich das Lambda am Ort von x fest, und dieser Ort bleibt nur gültig, solange x lebt. Genau diese Idee - ein Wert, der darauf verweist, wo etwas im Speicher lebt, und was passiert, wenn das, worauf er zeigt, verschwindet - ist genau das Thema der nächsten Seite. Wir begegnen den Zeigern frontal: wie man eine Adresse nimmt, wie man ihr folgt und wie dasselbe Problem der hängenden Referenz, das du gerade gesehen hast, überall in C++ auftaucht.
Häufig gestellte Fragen
Was ist ein Lambda in C++?
Ein Lambda ist eine anonyme Funktion, die du inline schreiben kannst, genau dort, wo du sie verwendest. Die Syntax lautet [captures](parameters){ body }. Es ist perfekt für kurze, einmalige Operationen - wie den Vergleicher, den du an std::sort übergibst - ohne dass du anderswo eine separate benannte Funktion deklarieren musst.
Was ist der Unterschied zwischen Capture per Wert und per Referenz in einem C++-Lambda?
[x] erfasst eine Kopie von x, eingefroren im Moment, in dem das Lambda erstellt wird. [&x] erfasst eine Referenz auf das ursprüngliche x, sodass das Lambda spätere Änderungen sieht und es verändern kann. Verwende [&] nur, solange garantiert ist, dass die erfassten Variablen das Lambda überleben, sonst bekommst du eine hängende Referenz.
Warum meldet mein C++-Lambda, dass es eine erfasste Variable nicht ändern kann?
Per-Wert-Captures sind innerhalb des Lambdas standardmäßig const. Füge das Schlüsselwort mutable hinzu - [x]() mutable { x++; } -, damit das Lambda seine eigene Kopie ändern darf. Beachte, dass dies nur die Kopie des Lambdas ändert, nicht die ursprüngliche Variable außerhalb.