0

Inspired by this question, I would like to compare the use of c++20 template lambda with a functor having a template operator().

As a test case, consider a template function call which takes a template lambda as an argument, and call this lambda instantiating it with some template parameters. The following c++20 code exemplifies the idea.

#include <tuple>
#include <utility>

template <int I, class Lambda, class... ArgsType>
void call(Lambda&& F, ArgsType&& ...args)
{
           F.template operator()<I>(std::forward<ArgsType>(args)...);
}

int main() {
   std::tuple<int, double, int> t{0,0,0};
   int a = 2;

   auto f = [&]<int I>(auto& x) { std::get<I>(x) += I + a; };
   call<0>(f, t);

   return 0;
}

In c++11/c++14/c++17, not having template lambda, the same task can be implemented with a functor, having a template operator(), as in the following code.

#include <tuple>
#include <utility>

template <int I, class Lambda, class... ArgsType>
void call(Lambda&& F, ArgsType&& ...args)
{
           F.template operator()<I>(std::forward<ArgsType>(args)...);
}

struct Functor {
    template <int I, class T>
    void operator()(const int& a, T& x) { std::get<I>(x) += I + a; };
};

int main() {
   std::tuple<int, double, int> t{0,0,0};
   int a = 2;

   Functor func{};
   call<0>(func, a, t);

}

The main disadvantage I see in the second example is that, to emulate the lambda capture, one needs to pass explicitly all local variables (in this case int a) to the functor. This can be tedious, if Functor::operator() needs many variables from its "owner". Eventually, one could also perhaps pass the pointer this to Functor::operator(). No such complications are present in the c++20 example, where the lambda capture takes care of capturing the needed variables.

Aside from simplicity, is there any other concrete difference between the two approaches outlined above? What about the efficiency?

max66
  • 65,235
  • 10
  • 71
  • 111
francesco
  • 7,189
  • 7
  • 22
  • 49
  • 4
    That's essentially the same comparison as between plain functors and generic lambdas, or (fully-deducible) templated functors and generic lambdas. – Quentin Jul 03 '19 at 08:28
  • @Quentin The comparison is perhaps similar, but for example, you cannot define a local class with member templates, so you need, as in the second code, to declare the functor outside main, whereas the c++20 lambda of the first code is local. – francesco Jul 03 '19 at 08:49
  • seems like you answer your own question. There is nothing wrong with that btw, but better put the answer in an answer instead of the question and comments... – 463035818_is_not_an_ai Jul 03 '19 at 09:03
  • C++14 already has generic lambdas which can be called with explicit template arguments (with the ugly `operator()<>` syntax). C++20 merely allows explicit template **parameter** lists for them (as is particularly helpful with concepts). – Davis Herring Jul 11 '19 at 17:58
  • @DavisHerring but how would it work with non-type template parameters? – francesco Jul 11 '19 at 21:25
  • @francesco: There can’t be an actual non-type parameter, since `auto` can’t produce one (although it could be `X<1>` from which the 1 might be extracted). I was simply responding to “In `c++11`/`c++14`/`c++17`, not having template lambda” since it seemed to imply that `F.template operator()<…>` is C++20-specific. – Davis Herring Jul 11 '19 at 21:43

1 Answers1

2

The main disadvantage I see in the second example is that, to emulate the lambda capture, one needs to pass explicitly all local variables (in this case int a) to the functor.

I disagree.

You can see a lambda almost as a class/struct with an operator() and some member corresponding to the captured variables.

So instead of

[&]<int I>(auto& x) { std::get<I>(x) += I + a; };

you can write (already from C++11)

struct nal // not a lambda
 {
   int const & a;

   template <int I, typename T>
   auto operator() (T & x) const
    { std::get<I>(x) += I + a; }
 };

and use it as follows

call<0>(nal{a}, t);

The main disadvantage I see with functors (and I suppose it's questionable that is a disadvantage) is that you can't simply capture, by reference or by value ([&] or [=]), all external variables but you have to explicitly list all the variable you use, both in defining the functor and in initializing the functor object.

Off Topic: observe that I've tagged const the operator() in my nal structure.

If you don't modify the captured variables, the lambdas are equivalent to functors with constant operator().

This is important if you pass the functor as constant reference

template <int I, class Lambda, class... ArgsType>
void call (Lambda const & F, ArgsType&& ...args)
{ // ......^^^^^^^^^^^^^^  now F is a constant reference
  F.template operator()<I>(std::forward<ArgsType>(args)...); // compilation error
                                                             // if operator()
                                                             // isn't const
} 

you get a compilation error if operator() isn't const

max66
  • 65,235
  • 10
  • 71
  • 111
  • On writing *The main disadvantage I see with functors...* I mean that you need to pass all the relevant variables to the functor, whatever the way you do that (constructor in your answer, or parameters to ```operator()``` in my code). If you change the code of the functor, and need more variables, you have to also change the declaration and/or the constructor. With capture [&] you get all of them automatically. – francesco Jul 03 '19 at 11:49
  • @francesco - OK: we are agree about this; what I want to say is that you don't need to pass the captured variables as arguments of the `operator()`. – max66 Jul 03 '19 at 11:52
  • By creating a reference inside the ```struct nal```, I think you have an extra overhead as compared to the lambda. – francesco Jul 03 '19 at 11:57
  • @francesco - I don't think there is a great overhead in a constant reference; anyway (1) as far I know, is exactly what happens with a lambda capturing by reference (except that with lambda all is done implicitly) and (2) also in you example (where you pass `a` as argument for `operator()`) you use a constant reference. – max66 Jul 03 '19 at 12:05
  • Yes, also passing ```a``` to ```operator()``` you use a reference, but the compiler *should* be able to optimize it out, while this is maybe more difficult if you create a reference in ```nal``` that persists after ```operator()``` has exited. – francesco Jul 03 '19 at 12:10
  • @francesco - I'm not a expert of compilers so I don't know which case is best optimized; anyway, a lambda capturing by reference has to create reference variable inside it; I don't see difference, in this case. – max66 Jul 03 '19 at 12:13
  • (The type of) a lambda expression **is** a class with an `operator()` and member variables per capture. – Davis Herring Jul 11 '19 at 17:55