A lambda expression can access local variables of the scope in which it is used. You can pass these variables by value or by reference to the lambda function. The following example shows how to sort a vector of signed integers. They should be sorted by the absolute values and in descending order. Therefore, an according lambda function was created. Additional, we want to know the number of function calls performed by the sort algorithm So we add a variable in local scope and pass a reference to the variable to the lambda expression.
int _tmain(int argc, _TCHAR* argv[]) { auto data = std::vector {-5, 1, 15}; int numberOfFunctionCalls = 0; auto AbsoluteDescendingComparer = [&numberOfFunctionCalls](int a, int b) { ++numberOfFunctionCalls; return abs(a) > abs(b); }; std::sort(data.begin(), data.end(), AbsoluteDescendingComparer); return 0; }
The first pair of brackets “[…]” is used to pass local variables of the lambda’s scope to the embedded lambda. The following syntax can be used
- “[&]” to pass all variables by references
- “[=]” to pass all variables by value
- “[&x]” or “[=x]” to pass the variable “x” by reference or by value
Evaluation in context of procedural and object-oriented programming
From a technical point of view, it is very easy to use local scope variables in lambdas. But if we think about good software design we may see some issues. My first concern is: we create hidden function parameters which will make it more difficult to see the dependencies between the different parts of the application. Why do we have this feature of passing variables in the “[…]” part at all? We can pass all parameters as function parameters anyway. So why don’t we use the normal way we know from functions? In my opinion, it is not necessary to have two ways to pass variables into a lambda function.
But what if we don’t want to use this feature? How can we implement a use case like shown in the example above? This would not be possible anymore. And I think that’s fine! From a software design point of view, I would say following: a lambda function is “a” function and therefore it should do one thing only – like any other function. In most (or all?) cases we use the “[…]” feature we smuggle in some values into the function because we need these values for a second functionality which will be executed by our lambda. So, our lambda function becomes additional responsibilities which contradicts the software design principle that a function should do one thing only. If we have such use cases like above, we should think about suitable components, like functors or classes. A functor or class can offer a second function which implements our second use case. In this case it can offer a getter method which tells about the number of function calls and a function to reset this counter.
In general, I can see two main kinds of use cases for the “[…]” feature. If we pass variables by value, we have a factory use case. That’s means we need these values to initialize or set up the behavior of our lambda. In this case I would prefer a functor with an according constructor which contains these parameters.
If we pass the values by reference, we want to get additional results at the end or during the execution of the function. In these cases, we add a second responsibility to the function. Therefore, a class would be a better choice because it can offer as many function as needed for this use case. Furthermore, it can hide implementation details, e.g. like in the case above we have to introduce a counter variable which will be increased at any function call. This is an implementation detail which should be hidden by a class and not seen by the client.
Furthermore, functions with references to scope variables are functions with side effects. And side effects create spaghetti code. I know, the term “spaghetti code” is original used for a confused program flow mainly caused by jump conditions, but I also use this term for a confused data flow. I think, in modern software, a messy data flow caused by functions with side effects, is the main reason for erroneous code and for legacy code which is nearly not maintainable.
Evaluation in context of functional programming
Till now I expressed a lot of negative thoughts about scope variables. But of course, this feature does not exist without a reason. If we use it in a suitable way it can offer some advantages. These advantages come to light if we implement in a functional style. Within the example above, the lambda was embedded and executed directly within the parent function. This procedural way of implementation is normally done by function calls with function input parameter. But the conditions change if we don’t execute the lambda directly. Instead, our parent function should now return a pointer to the lambda function. So, the client will be able to store this function pointer and execute the function call later. This functional way of implementation can benefit from the possibility to use the parent scope where a function is embedded. This feature is called “closure”. In short it means, that a function can exists including its surrounding context. Therefore, in this functional programming context, the variables are no longer “passed” to the function, they are “captured”.
At this point I want to stop and don’t want to go into details about closures and functional programming with C++, as this is an own topic and will go beyond the scope of this article. But there will be a further article with the topic “closures in C++”.
Of course, I mentioned this topic in this short section to show the real use case for scoped variable access in lambda functions. But in most cases, we use C++ to implement in a procedural or object-oriented way. And within this programming paradigms, the scoped variable access most often results in more disadvantages then advantages.
Summary
Lambda functions offer a mechanism to pass variables of the local scope into the lambda function. From a technical point of view this feature is easy to use and allows implementation of lambdas in much more use cases. From a software design point of view, one should ask if all these use cases should be implemented by using lambdas at all or if there are much better alternatives. Therefore, some see this feature as a “must have” while others see it as a way to write obscure and error-prone code. And in my opinion, both are right.
Pingback: Lambda Closures in C++ | coders corner