Under the covers of C++ lambdas - Part 3: Generic lambdas

In this post, we continue to explore lambdas and compare them to function objects. In the previous posts, Under the covers of C++ lambdas - Part 1: The static invoker, we looked at the static invoker, and in Under the covers of C++ lambdas - Part 2: Captures, captures, captures. Part 3 takes a closer look at generic lambdas.

This post is once again all about under the covers of lambdas and not about how and where to apply them. For those of you who like to know how they work and where to use them, I recommend Bartłomiej Filipek's book C++ Lambda Story.

In the last post, we ended with a score of Lambdas: 2, Function objects: 0. Let's see how that changes with today's topic.

Generic lambdas were introduced with C++14 as an extension to lambdas. Before C++20, it was the only place where we could use auto as a parameter type. Below, we see a generic lambda:

1
2
3
4
5
6
int main()
{
  auto lamb = [](auto a, auto b) { return a > b; };

  return lamb(3, 5);
}

Because lamb's parameters are generic, we can use it with any type (a) that provides an operator > for the other type (b). In generic code, where we do not always know the type because the code is generic, C++14's generic lambdas are a great improvement.

This post is about lambdas under the covers, so let's not focus on all the cool application areas for generic lambdas. Let's answer the question, "What is an auto parameter?". At first glance, it looks somewhat magical, or at least it did to me when I first saw it. At this point, we can refer to C++ Insights to see what the example above looks like when the compiler processed it:

 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
29
30
31
32
33
34
int main()
{
  class __lambda_3_15
  {
    public:
    A A method template with two individual type template parameters
    template<class type_parameter_0_0, class type_parameter_0_1>
    inline auto operator()(type_parameter_0_0 a, type_parameter_0_1 b) const
    {
      return a > b;
    }

    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline bool operator()(int a, int b) const
    {
      return a > b;
    }
    #endif

    private:
    template<class type_parameter_0_0, class type_parameter_0_1>
    static inline auto __invoke(type_parameter_0_0 a, type_parameter_0_1 b)
    {
      return a > b;
    }
    public:
    // inline /*constexpr */ __lambda_3_15(__lambda_3_15 &&) noexcept = default;

  };

  __lambda_3_15 lamb = __lambda_3_15(__lambda_3_15{});
  return static_cast<int>(lamb.operator()(3, 5));
}

In the transformed version above, we can see at A the magic behind an auto parameter. The compiler makes this method a template, which, by the way, is also true for C++20's abbreviated function template syntax, as the name probably gives away. The compiler adds a type template parameter to the created method template for each auto parameter.

Ok, now we can say that this is nothing special. We, as users, can write method templates as well. So, this time, there is no advantage of lambdas over function objects, right? Wrong! Yes, in general, we can write method templates, of course. But where can we write them, and where can the compiler create them?

We are not allowed to create local classes with method templates. Only lambdas, and with that, the compiler, is allowed to create such a thing. This restriction is there intentionally, as the paths lambdas take are much more limited than allowing it for all users. However, there is an attempt to lift this restriction. See P2044r0 for more details.

The restriction of local classes with method templates is an issue for C++ Insights, which led to this issue #346. C++ Insights creates lambdas where the compiler tells it, in the smallest block scope. We can see this in the transformation above. This behavior is mandated by the standard [expr.prim.lambda.closure] p2:

The closure type is declared in the smallest block scope, class scope, or namespace scope that contains the corresponding lambda-expression. ...

This is a kind of chicken-egg problem. Moving the lambda out is far from trivial, and there is no guarantee for successfully compiling code. Leaving it in is a guaranteed error during compilation. As both versions are somewhat wrong, I chose to show them where the compiler says, in the smallest block scope, and take that known error. I also hope that the restriction for method templates will be lifted with C++23.

I hope that the last posts helped you see that the compiler is, in fact, a powerful friend to us. Yes, we can create something close to lambdas with function objects, but the compiler is still more efficient and better.

This final comparison round goes to lambdas as the other two before. We have a final score of:

Lambdas: 3, Function objects: 0

Summary

Yes, we can emulate lambdas with function objects. Most of it is the same for lambdas. However, created and maintained by the compiler, lambdas are more powerful. To say it with Bartek's words:

... in general, the lambda closure type generated by the compiler is a bit magical...

Support the project

Have fun with C++ Insights. You can support the project by becoming a GitHub Sponsor or, of course, with code contributions.

Andreas