Falsch gefangen

Was passiert eigentlich, wenn man sich nicht an die Regeln hält? Wenn man bspw. in einer Capture List einer lambda-Funktion Referenzen verwendet, die dann aus dem Scope gehen? Die Antwort lautet: keine Ahnung. Oder etwas formaler: undefined behaviour. Theoretisch darf der Compiler alles mögliche machen, praktisch wird von “einfach funktionieren” bis “durch falsche Daten Unsinn tun” alles mögliche dabei sein.

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
27
28
#include <iostream>
#include <string>
#include <functional>
 
std::function<void(std::string const &)> createFn()
{
  int x = 5;
  auto fn = [&](std::string const &msg) { std::cerr << msg << x << std::endl; };
 
  fn("createFn: ");
 
  return fn;
}
 
void test(std::function<void(std::string const &)> fn)
{
  int y = 20;
  fn("test: ");
}
 
int main()
{
  auto fn = createFn();
 
  test(fn);
 
  fn("main: ");
}

Erklärung

Das Problem hier im Code tritt in der Funktion createFn auf: diese erzeugt in Zeile 8 ein lambda-Funktionsobjekt, welches über Referenzen eine lokale Variable der Funktion fängt (x). Das ist soweit erstmal kein Problem, solange der Kontext (in dem Fall: der Stackframe der Funktion) noch existiert. In Zeile 12 wird das Funktionsobjekt allerdings aus der Funktion zurückgegeben. Damit wird seine Lebenszeit über die Existenzdauer des lokalen Kontextes raus verlängert. Das wäre an sich auch noch kein Problem, würde nicht die gefangene Variable x nur in diesem Kontext existieren. Wenn das Funktionsobjekt nun in einem anderen Kontext verwendet wird, so zeigt die Referenz irgendwohin. Im Prinzip verhält sie sich wie ein Pointer, der falsch initialisiert wurde (letzten Endes wird sie hinter den Kulissen auch genauso realisiert: als Pointer, der auf die Adresse der Variable x initialisiert wird). Wird nun nach der Zerstörung der lokalen Variable x die Adresse wieder anderweitig verwendet, dann kommt es dazu, dass unser Funktionsobjekt irgendwelche Daten ausliest. Daher ist die Ausgabe nicht wirklich vorherzusehen und ändert sich potentiell auch, wenn das Programm mit anderen Einstellungen übersetzt wird (wie im Video demonstriert).

Würden wir in der lambda-Funktion die Variable schreiben, wären die Auswirkungen potentiell noch schlimmer. Im günstigsten Fall crasht das Programm (wenigstens ein definierter Zustand). Im ungünstigsten Fall überschreiben wir fremde Variablen (oder Teile davon) und rechnen dann mit falschen Werten weiter. Derartige Fehler sind schwer bis nicht sinnvoll zu finden.

Die Lösung des Problems ist einfach, aber möglicherweise teuer: statt Referenzen müssen wir Kopien in das Funktionsobjekt übernehmen. Im Falle unseres Integers hier ist das kein Problem. Wenn wir mit größeren Objekten hantieren, dann kann das möglicherweise teuer werden. Da gilt es dann abzuwägen, ob so eine Realisierung überhaupt funktioniert oder ob wir ggf. eine andere Lösung finden müssen.

Was im übrigen kein Problem wäre: wenn die Funktion createFn ihrerseits die Funktion test direkt aufrufen würde. Dabei würde der Stackframe von createFn ja während der Laufzeit der Funktion test noch existieren und die Referenz wäre gültig. Erst beim endgültigen Verlassen von createFn wird die Referenz zum Problem.