Lambda and Closure


Lambda expressions come from the lambda calculus and can be resumed as anonymous functions [1]. This means it does not have a name, so it can't be called several times unless you copy and paste the code every time you need it, or you set it to a variable.

Moreover, lambdas returns a closure to a variable you assign to hold it, that is a concept, in my opinion, similar to scope and visibility range. A lambda closure comprehends the set of variables the expression can see. This set can be limited to it's own parameters or variables declared inside the lambda instructions, or limited by the surrounding context that defined the lambda expression. In the first case, we say the expression is closed, and in the second, it is open until all the free variables (which are from the surrounding context) are defined [1]. The act of defining free variables closes the expression [2] and it is made through captures. The capture does what the name says: grab the variable definition at the bigger environment and expands the lambda sight capacity, therefore, it can access the memory address. 

According to Meyers' book [3], in C++, you can capture a variable by copy or by reference. And here, the trouble begins. Depending on where your capture variable is defined, the closure coming from a lambda expression can leave a dangling reference. If the content of that reference is deleted during the runtime, the lambda expression may try to access an invalid memory address, and this will give you a segmentation fault. This happens if the captured variable (doesn't matter if it is passed by reference or value) dies before the lambda closure. In the book's example, the lambda expression may live longer than expected because the lambdas are stored in a vector of functions and they depend on an attribute of an object that may die (be deleted) before manipulating the vector. 

To try to avoid this segmentation fault, you can explicitly [4] list the variables and parameters which the lambda is dependent on [3], just to remember you not to copy and paste it in other parts of your code without paying attention. The compiler cannot predict (yet) this kind of error, nor a static analyzer. This practice can also avoid capturing the whole object by accident, due to the option of explicitly writing the "this" pointer or not [3].

So, to really avoid all of this trouble, is using move semantics to move objects and biding the lambda lifetime to the object lifetime, and this is called "init capture" [3]. Don't be fooled by the name "move" because there is no movement at all. This is a subject that deserves it's own dedicated page, and you can see it here (working on the content yet).

As final regards, lambda expressions can be tricky, but they are better to read than an equivalent code without them [3]. They don't always have to be written using the move semantics, specially if the lambda is an argument for a STL algorithm (because they are safe for not leaving dangling references) or if you are using it to throw an exception [3]. Everything is up to the programmer to decide what to use, and the best decision comes with knowledge. 

If you want to know more, check the cprogramming.com notes on lambda expressions on C++.

[3] Scott Meyer - Effective and Modern C++
[4] Note: C++ is all about being explicit.

No comments:

Post a Comment

Global Game Jam 2024

Wow, what a weekend. The Global Game Jam is intense. But I and 3 other members manage to build a game for the theme "Make me laugh&quo...