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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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.