Lambda Overdose

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 a 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 the functions as 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 functor – erm, function object, sorry, Jackie – 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.

Previous Post
Next Post
Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

25 Comments


  1. 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.

    Reply

  2. Why do we have rvalue reference inside for loop instead of simple const reference? Is there any advantage of it?

    Reply

  3. 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.

    Reply

    1. The problem you describe here can easily be solved with code folding. just fold those lambdas.

      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.

      If you move those lambdas out, you increase the scope they can be used.

      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.

      Also having to jump around in the code base to follow the program flow doesn’t make code easy to understand.

      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.

      Reply

      1. Thanks for taking the time to write exactly what I would have responded πŸ™‚

        Reply

          1. Not at all πŸ™‚


  4. The Zen philosopher, Basho, once wrote, ‘A flute with no holes, is not a flute. A donut with no hole, is a Danish.’

    So, what is a named lambda with no capture?

    Reply

  5. 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.

    Reply

  6. 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” :-).

    Reply

    1. 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”

      Reply

      1. I think though that it might derail quickly into some discussions whether

        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.

        Reply

  7. 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.

    Reply

  8. “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? πŸ™‚

    Reply

    1. Because I suck at names πŸ™ Didn’t want to spend more time on the name then on the rest of the post. And that way it’s a more realistic example πŸ˜‰

      Reply

  9. I think usage of lambdas (anonymous fonctions) becomes suspect when they are given a name (no longer anonymous!)

    Reply

    1. 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.

      Reply

  10. 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.

    Reply

    1. 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.

      Reply

      1. 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) { …. };

        Reply

        1. 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.

          Reply

          1. 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.


          2. 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.

Leave a Reply