Umgebung einfangen
Lambda-Funktionen in C++ beherrschen einen netten Trick, der sie unglaublich viel praktischer macht, als einfache Funktionspointer: sie können Teile ihrer Umgebung "einfangen" und im Funktionsobjekt verpacken. Auf diese Art und Weise kann man Kontext, der zum Zeitpunkt der Erzeugung der Lambda-Funktion bestand, später noch verwenden.
Video
Code
#include <iostream>
int main()
{
int x = 10;
int y = 3;
auto f1 = [=]() {
std::cerr << "Copy: " << x << " " << y << std::endl;
};
auto f2 = [&]() {
std::cerr << "Ref: " << x << " " << y << std::endl;
};
auto f3 = [&x, y] {
std::cerr << "Mixed: " << x << " " << y << std::endl;
};
x = 5;
y = 9;
f1();
f2();
f3();
}
Erklärung
Die eckigen Klammern, mit denen eine Lambda-Funktion eingeleitet wird, sind
nicht für umsonst dort. Sie können die sogenannte Capture List enthalten:
Anweisungen an den Compiler, wie und welche Teile der Umgebung in das
resultierende Funktionsobjekt zu übernehmen sind. Die Funktionen f1 und f2
demonstrieren die zwei gegensätzlichen Varianten: grundsätzlich die komplette
Umgebung steht zur Verfügung (welche Variablen tatsächlich zu übernehmen sind,
ergibt sich aus der Verwendung in der Funktion), einmal als Kopie, einmal als
Referenz. Enthält die Liste ein =, so werden alle verwendeten Variablen als
Kopien in das Funktionsobjekt übernommen. Für die Funktion sichtbar sind als die
inhalte der Umgebung zum Zeitpunkt der Erzeugung des Funktionsobjektes. Ganz
anders hingegen die Variante f2. Enthält die Capture List ein &, so wird der
gesamte Kontext als Referenzen verfügbar gemacht. Das kann massiv Kopieraufwand
sparen (vor allem bei großen Objekten). Für die Funktion sichtbar werden
allerdings die Inhalte der Umgebung zum Zeitpunkt des Aufrufes. Diese kleine,
aber wichtige Unterschied macht die zweite Variante etwas gefährlich: wenn
bestimmte Variablen nicht mehr existieren (weil sie bspw. im Kontext einer
Funktion bestanden und nach deren Verlassen aufgeräumt wurden), dann hängen die
betreffenden Referenzen in der Luft. Der Zugriff führt dann im günstigsten Fall
zu einem Programmabsturz, im ungünstigsten zu einer Korruption von fremden
Daten. Hier muss man also massiv aufpassen, welche Variante man wirklich braucht
und ob im Falle der Referenzen garantiert werden kann, dass der gesamte Kontext
für die Lebensdauer des Funktionsobjektes Bestand hat.
Die letzte Variante f3 ist eine Mischung aus den ersten beiden: x wird als
Referenz, y als Kopie gefangen. Folgende Werte werden die Funktionen im
Beispiel zu sehen bekommen: f1 sieht 10 und 3, wie bei der Initialisierung
des Funktionsobjektes bestehend. f2 sieht 5 und 9, wie sie zum
Aufrufzeitpunkt in Zeile 24 gesetzt sind. f3 sieht ein gemischtes Bild: 5
und 3, da die Änderung von x sichtbar wird, die von y aber nicht.