Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Recursive Variadic Lambda

In C++ we can have a recursive variadic template function. For example:

template<typename ...H>
void g(){}
template<typename H, typename ...T>
void g(H h,T ...t){
    std::cout << h << "\t";
    g<T...>(t...);
}

However this does not seem possible to be done using lambdas. My two main main concerns are:

  • How to establish a base-case.
  • How to make a lambda be recursive and at the same time variadic.

I know I can have recursive lambdas but can't see a way to make it variadic. Is this type of feature only available in more high-level kind of languages such as Javascript?

Edit: So far, this is what I have come up with:

template<typename C,typename H, typename ...T>
std::function<void(C,H,T...)> f=[](auto&&  self,H h,T ...t){
    std::cout << h << "\t";
    if(sizeof...(t)>0)
        self(self,t...);
};

Here the first argument would be the lambda itself. However, the main issue is that in order to call this method I would to have define the type C, which I'm unsure on how to do (or even if it's possible).

Edit: A more simplistic approach would be:

auto f = [] (auto&&  self, auto&& h,auto&&... t) {
    std::cout << sizeof...(t) << "\n";
    if( sizeof...(t)>0 ){
        self(self,1);
    }
};

int main()
{
    f(f,1,2,3,4,5,6);
    return 0;
}

However gives the following error:

main.cpp:55:13: error: use of ' [with auto:1 = &; auto:2 = int; auto:3 = {}]' before deduction of 'auto'
     self(self,1);
         ^
like image 939
Leonardo Faria Avatar asked Mar 26 '18 15:03

Leonardo Faria


2 Answers

C++17 solves this problem with Constexpr if:

#include <iostream>

auto f = [](auto&&... t){
    auto f_impl = [](auto& self, auto&& h, auto&&... t) {
      std::cout << sizeof...(t) << "\n";
      if constexpr ( sizeof...(t)>0 ){
        self(self,t...);
      }
    };
    return f_impl(f_impl, t...);
};

int main() {
    f(1,2,3,4,5,6);
}

I also took the liberty of wrapping the first parameter(self) inside a "parent" lambda.

The problem with using if(sizeof...(t)) as a guard is that, even if you will not call the lambda with improper arguments at runtime, the compiler still needs to compile the expression self(self, t...) with sizeof...(t)==0, which fails.

constexpr if solves this by making this check at compile-time, and not even compiling the block when the check yields false. Before C++17, conditional compilation semantics(excluding macros) could only be achieved using SFINAE or template specialization, both of which can't be done using only lambdas.

like image 189
Cássio Renan Avatar answered Oct 23 '22 06:10

Cássio Renan


I hate helpers, i think using helpers is a misleading design. That said, i couldn't find a solution to this without using a small helper.

The obstacles encountered here are the following:

  • You cannot refer to a variable declared with auto within it's own initialisation expression

    This is solved by passing the function as a parameter to itself using an inner lambda to provide a clean public interface

  • You cannot use if( sizeof... ) as a guard (or any other "runtime" mechanism) for the "base-case" because you would still have a call to a function that cannot accept that number of parameters within that if block, which doesn't compile

    This is solved by "overloading the lambda" and actually defining all the needed function overloads (i.e the "base-case" also)

  • You cannot "simply" overload a lambda once it is defined

    This is solved by using a "small" helper code

All in all, one solution could be this:

#include <iostream>

/////////// Helper

template<class F, class... Fs>
struct overloaded : F, overloaded<Fs...>
{
  using F::operator();
  using overloaded<Fs...>::operator();

  overloaded(F&& f, Fs&&... fs)
    : F(std::move(f))
    , overloaded<Fs...>(std::move(fs)...)
  {}
};

template<class F>
struct overloaded<F> : F
{
  using F::operator();

  overloaded(F&& f)
    : F(std::move(f))
  {}
};

template<class... Ts>
overloaded<Ts...> overload(Ts&&...lambdas)
{ return overloaded<Ts...>{std::move(lambdas)...}; }

///////// Recursive Variadic Lambda

int     main(void)
{
  auto lambda = [](auto... args)
    {
      auto lambda_impl = overload(
        [](auto self)
        {
        },
        [] (auto self, auto first, auto...rest)
        {
          std::cout << first << std::endl;
          self(self, rest...);
        });

      lambda_impl(lambda_impl, args...);
    };

  lambda(4, "lol", 8.3);
}
like image 2
Drax Avatar answered Oct 23 '22 07:10

Drax