Logo

Blog


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

In this post, we are continuing to explore lambdas and comparing 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 by 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's 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. Llet's answer the question "what is an auto parameter?". At first glance it looks somewhat magical, 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 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. For each auto parameter, the compiler adds a type template parameter to the created method template.

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 no guarantee for successful 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 have hope that the restriction for method templates gets lifted with C++23.

I hope that and the last posts helped you to see, that the compiler is, in fact, a powerful friend for 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 Patreon or, of course, with code contributions.

Andreas