Contents
Lambdas are a nice recent addition to C++. They are cool, they are hip, and they tend to be overused and misused.
Since lambda expressions came up in C++11 and got a huge boost in usability in C++14, they have been all the rage. Don’t get me wrong. Lambdas really are useful and cool and everything. But reading blog posts, the CppLang Slack channel and other sources lately has given me the impression that some people use lambdas in ways they should not be used.
Lambdas are not a replacement for normal functions
Let’s have a look at this example:
int main() {
auto sequence = [](size_t i){
std::vector<size_t> result(i);
std::iota(begin(result), end(result), 0);
return result;
};
auto print = [](auto const& container) {
for (auto&& e : container) {
std::cout << e << ' ';
}
std::cout << '\n';
};
print(sequence(22));
}
Here, the main
function contains the definition of two lambdas that act like normal functions. The actual work done in main
is only the single last line, but the function body is blown up to 14 lines total. If a reader wants to know what main
does they have to skim past the lambdas, which takes unnecessary time. It can be worse, e.g. if the code of interest is interleaved with lambda definitions:
int main() {
auto sequence = [](size_t i){
std::vector<size_t> result(i);
std::iota(begin(result), end(result), 0);
return result;
};
auto s = sequence(22);
s.push_back(42);
auto print = [](auto const& container) {
for (auto&& e : container) {
std::cout << e << ' ';
}
std::cout << '\n';
};
print(s);
}
Now there is an extra strain on the reader to determine which lines are important to read and which are not. Let’s have a look at a more natural implementation:
auto sequence(size_t i) {
std::vector<size_t> result(i);
std::iota(begin(result), end(result), 0);
return result;
}
template<class C>
auto print(C const& container) {
for (auto&& e : container) {
std::cout << e << ' ';
}
std::cout << '\n';
}
int main() {
auto s = sequence(22);
s.push_back(42);
print(s);
}
This is pretty much the same code, besides the little boilerplate needed to declare print
and template
. The readability has improved vastly, though: main
are just three lines that may be enough to know what is going on. If you need to know what sequence
does exactly, because it’s poorly named, then you can look into the function as usual.
There might be two little cons to having actual functions instead of lambdas: The names of the functions will be visible to the linker outside the translation unit, i.e. they have external linkage, which also can affect inlining. Secondly, look up rules may differ, which may actually be of concern for something named print
. Both issues can, however, be easily remedied by using anonymous namespaces for internal linkage and a named namespace for the look up, if absolutely necessary.
Extra long lambdas
A variant of the above problem is to make necessary lambdas longer than a few lines. Even if you need to use a lambda, e.g. because you have captures and/or you actually need the function object it creates, lambdas should be short, even shorter than your average function.
The reason is that lambdas usually are only one element of a larger context, e.g. an algorithm call. If a single element is larger than the whole rest of its context, the reader will focus on the single element instead of that context. In addition, larger lambda bodies are likely to have a lower level of abstraction than the surrounding function, so the function as a whole violates the SLA principle.
There is nothing that prohibits extracting functions from a lambda body like you would do from a normal function to keep it short and readable.
The functional hype
There are certain people out there that revel in the sweet purity of functional programming. It is very rare to see a conference without at least a few talks about functional C++ these days. People start comparing C++ to Haskell and produce lambdas that return lambdas that generate other lambdas that … you get the gist.
Functional principles are a very interesting topic (I attend or watch those talks myself whenever I come across one), and it is really great to have such facilities in the language. Some of them are even needed to write scalable parallel software. But C++ is not and never will be a functional language, like it is not an object oriented language, either.
Instead, C++ is a multi-paradigm language. It is a box full of different tools, and we do best to use them only where they are appropriate and in the way that makes the best use of them. There is no sense in using a single tool (e.g. lambdas) everywhere we can, in every possible way, only because we finally have it at our disposal.
Permalink
FYI: The link above with the link text “erm, function object, sorry, Jackie โ” redirects to a pishing side, which I guess is not intended.
Permalink
It appears that Jackie has taken her blog offline ๐
Permalink
So in summary lambdas shall be used for rarely used and very short functionality and likely be tagged as such to move them out if every needed again?
Thought them mainly for being like filters or other logics constrained operations which highly unlikely will be used again.
Permalink
I agree with the key point: lambdas are not a replacement for functions. But with one exception to the rule that lambdas should be as short as possible. This rule does not apply to the use of “Inline Visitor Pattern”. In this case, lambdas should be as long as needed. After all, the key idea of this pattern is to avoid separate functions for each type.
Permalink
Why do we have rvalue reference inside for loop instead of simple const reference? Is there any advantage of it?
Permalink
The problem you describe here can easily be solved with code folding. just fold those lambdas.
If you move those lambdas out, you increase the scope they can be used.
Also having to jump around in the code base to follow the program flow doesn’t make code easy to understand.
Permalink
The need to use IDE features to make the code kinda-readable is IMNSHO by far the worst thing which can happen to readability (really? when making a code review, I need to jump around parsing lambdas and clicking the IDE to make somebody-else’s job of making the code readable in the first place? Even worse – anybody-else-except-for-the-author needs to repeat this process for themselves on each of the workstations?). Also – there are also source control diffs where there is no code folding at all.
In practice, it is really difficult to get into scoping/collision problems before the project gets to a 1M of LOC; and if scoping ever becomes a problem – it is namespaces (and not lambdas) which are intended (and used) to solve it. Also – in case of nameable functions, I’d argue that it is an advantage (enabling re-use) rather than a disadvantage.
a. whenever proper naming is available for these functions – in 99% of the cases you shouldn’t need to “follow the program flow” as a step-by-step debugger would do – the code is perfectly readable with just those three lines of code (and 3 lines in the 2nd example are significantly more readable than 15 lines in the first one).
b. IF you need to follow the code as if step-by-step debugger would do – even the first example above causes “jumping around” you’re referring about (as the flow is not linear, it is just the length of those jumps which changes); and if you rewrite it without named lambdas and will use them in-place all within one call instead – then it will become not just “poorly readable”, but “utterly unreadable”. In general, as soon as the things are nameable – they SHOULD be named, and this tends to improve readability a lot; lambdas are not an exception.
It is when the lambdas are unnameable (=”there is no good immediately obvious name for them”) which changes the whole thing in favor of lambdas.
Permalink
Thanks for taking the time to write exactly what I would have responded ๐
Permalink
Oops – sorry for stealing the opportunity to reply ๐
Permalink
Not at all ๐
Permalink
So, what is a named lambda with no capture?
Permalink
…a function? (…an anonymous function?)
Permalink
I think local, named lambdas are a natural way to express things that I formerly did with preprocessor macros that I used inside a single function, like a kind of nested function.
Permalink
Thanks! IMO – a long-overdue discussion :-). BTW (complaints, complaints ;-)) IMO it would be even better to make another post, listing a few specific cases when lambdas are obviously useful – and saying that “if you’re going to use lambdas outside of these few well-defined cases – think twice because it can easily be an overlambdibifaction” :-).
Permalink
Thanks for the suggestion. I will consider the list of classic use cases for lambdas. I think though that it might derail quickly into some discussions whether I should have added use case X or left out use case Y. So I think this is a good start to raise at least the awareness that there might be such a thing as “overlambdification”
Permalink
There is always such a risk – but OTOH trying to please everybody very quickly leads to doing nothing out of fear to displease somebody ๐ . There was a story about a village wizard who promised to get villagers any weather they want – provided that they all agree on the weather they want; as the story goes – he never needed to do anything.
Permalink
Helper lambdas that can be promoted to a library of helper functions may benefit from this by becoming directly accessible and testable by unit tests.
Permalink
“If you need to know what sequence does exactly, because itโs poorly named, then you can look into the function as usual.”
Why don’t give it a better meaningful name, then? ๐
Permalink
Because I suck at names ๐ Didn’t want to spend more time on the name than on the rest of the post. And that way it’s a more realistic example ๐
Permalink
I see ๐ I suck at names too ๐
But thinking about good names is a good exercise. Read, e.g., this great article:
http://www.carlopescio.com/2011/04/your-coding-conventions-are-hurting-you.html
Permalink
I think usage of lambdas (anonymous fonctions) becomes suspect when they are given a name (no longer anonymous!)
Permalink
except when the name is needed to something else. I use locally named lambda in my “make a tuple out of a lambda trick” for ex.
Permalink
Lambda returning lambda have valid use case in “normally complex” C++. A compose(f,g) function for example or usng lambda as local, struct-less type erasure.
Rest is indeed overlambdification.
Permalink
While I think that these often could also be functions returning lambdas, there certainly are some use cases. I still think that many of the examples we see today do not need the lamdification and are unnecessarily complex for the reader. Of course, when it comes to squeezing the last bit of performance out of some code, e.g. in library code, there may be more cases where lambdas are appropriate.
Permalink
Of course ! You also have these cases where, yeah you need the lambda returning lambda in a context a lambda is welcome. I agree they’re rather rare.
Another idioms that I think coudl be valid is having const lambda value as function object in a library so you don’t have to write all the cuft of a templated function object.
// mylib.h
const auto my_func = [](auto a, auto b) { …. };
Permalink
Indeed I’ve seen an article by Eric Niebler were he proposed to have such objects to deal with “using std::begin; begin(rng);” kind of thing. And I believe Ranges TS (and now Executors proposal) are actually adding this to the standard library.
Permalink
In the case of templated functions in headers, anonymous namespaces go out the window.
Lambdas are really the only way to write helper functions specific to a single template function without polluting the global namespace, unless you want to relegate yourself to only instantiating the template for a few predefined types by moving its definition out of the header.
The use cases of these are fairly rare, and on more than one occasion I’ve discovered the helper has applicability beyond just one function, and should be moved to a utility-like namespace.
Permalink
For helpers that are useful beyond the scope of your current use, I’d definitely recommend putting them in a helper library. Having them as lambdas in function scope delimits their usefulness way too much. Helpers that have no use beyond your use case can always go into a
namespace detail
to prevent name pollution – that is a practice widely used e.g. in Boost.