Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

generating calls to lambdas with arbitrary number of parameters

The following definition has proven to be very useful for me:

template<class Func, class... Args>
void apply_on_each_args(Func f, Args... args)
{
    (f(args), ...);
}

Basically, the arguments pack folded on comma operator, allows to define several calls to a function taking an argument. For example:

apply_on_each_args([] (auto x) { cout << x << endl; }, 1, 2, "hello");

will call the anonymous lambda on 1, 2 and "hello".

That idea presented, I would like to do the same thing but passing lambdas taking two, three, and so on arguments. For example, something like that

apply_on_each_args([] (auto x, auto y) { /* use x and y */ }, 1, 2, "hello",  "bye");

Any clue, technique, idea, etc that allow achieving it?

like image 298
lrleon Avatar asked Jun 23 '18 18:06

lrleon


2 Answers

A way to make a apply_on_each() that receive a lambda (or a function) that receive an undefined number of generic arguments and call they (partially) unfolding in a C++17 way.

To be honest, it's only the generalization of the Bolov's voodoo answer.

First of all, a set of constexpr functions to detect the number of arguments of a function (supposing the arguments are generic, so supposing a list of integer zeros is acceptable)

template <typename F, typename ... Ts>
constexpr auto numArgsH (int, Ts ... ts)
   -> decltype( std::declval<F>()(ts...), std::size_t{} )
 { return sizeof...(Ts); }

template <typename F, typename ... Ts>
constexpr auto numArgsH (long, Ts ... ts)
 { return numArgsH<F>(0, 0, ts...); }

template <typename F>
constexpr auto numArgs ()
 { return numArgsH<F>(0); }

Now the apply_on_each() function that detect the number of arguments for the function func and, following the Bolov's example, call a (first) helper function adding a (double, in this generalization) list of indexes and the std::tuple of arguments

template <typename F, typename ... Ts>
void apply_on_each (F func, Ts ... ts)
 {
   static constexpr auto num_args { numArgs<F>() };

   apply_on_each_h1(func,
                    std::make_index_sequence<sizeof...(Ts)/num_args>{},
                    std::make_index_sequence<num_args>{},
                    std::make_tuple(ts...));
 }

Now the first helper function that "unpack" the first index sequence, using C++17 folding, and call the second helper function

template <typename F, std::size_t ... Is, std::size_t ... Js, 
          typename ... Ts>
void apply_on_each_h1 (F func,
                       std::index_sequence<Is...> const &,
                       std::index_sequence<Js...> const & js, 
                       std::tuple<Ts...> const & t)
 { (apply_on_each_h2<Is>(func, js, t), ...) ; }

Now the last helper function that, playing with indexes, call the func with the right arguments

template <std::size_t I, typename F, std::size_t ... Js, typename ... Ts>
void apply_on_each_h2 (F func,
                       std::index_sequence<Js...> const & js, 
                       std::tuple<Ts...> const & t)
 { func(std::get<I*sizeof...(Js)+Js>(t)...); }

The following is a full example

#include <tuple>
#include <utility>
#include <iostream>
#include <type_traits>

template <typename F, typename ... Ts>
constexpr auto numArgsH (int, Ts ... ts)
   -> decltype( std::declval<F>()(ts...), std::size_t{} )
 { return sizeof...(Ts); }

template <typename F, typename ... Ts>
constexpr auto numArgsH (long, Ts ... ts)
 { return numArgsH<F>(0, 0, ts...); }

template <typename F>
constexpr auto numArgs ()
 { return numArgsH<F>(0); }

template <std::size_t I, typename F, std::size_t ... Js, typename ... Ts>
void apply_on_each_h2 (F func,
                       std::index_sequence<Js...> const & js, 
                       std::tuple<Ts...> const & t)
 { func(std::get<I*sizeof...(Js)+Js>(t)...); }

template <typename F, std::size_t ... Is, std::size_t ... Js, 
          typename ... Ts>
void apply_on_each_h1 (F func,
                       std::index_sequence<Is...> const &,
                       std::index_sequence<Js...> const & js, 
                       std::tuple<Ts...> const & t)
 { (apply_on_each_h2<Is>(func, js, t), ...) ; }

template <typename F, typename ... Ts>
void apply_on_each (F func, Ts ... ts)
 {
   static constexpr auto num_args { numArgs<F>() };

   apply_on_each_h1(func,
                    std::make_index_sequence<sizeof...(Ts)/num_args>{},
                    std::make_index_sequence<num_args>{},
                    std::make_tuple(ts...));
 }

int main()
 {
   auto l1 = [](auto a)
    { std::cout << "- l1:" << a << std::endl; };

   auto l2 = [](auto a, auto b)
    { std::cout << "- l2:" << a << ", " << b << std::endl; };

   auto l3 = [](auto a, auto b, auto c)
    { std::cout << "- l3:" << a << ", " << b << ", " << c << std::endl; };

   apply_on_each(l1, 1, 2l, 3ll, "4", '5', 6.0);
   apply_on_each(l2, 1, 2l, 3ll, "4", '5', 6.0);
   apply_on_each(l3, 1, 2l, 3ll, "4", '5', 6.0);
 }
like image 153
max66 Avatar answered Nov 14 '22 07:11

max66


Ok, my voodoo is strong tonight:

auto foo(int, int) -> void;

template <class Func, class... Args, std::size_t... I>
void apply_on_2x_indexes(Func f,  std::index_sequence<I...>, std::tuple<Args...> t)
{
    (f(std::get<I * 2>(t), std::get<I * 2 + 1>(t)), ...);
}

template<class Func, class... Args>
void apply_on_each_2_args(Func f, Args... args)
{
    apply_on_2x_indexes(f, std::make_index_sequence<sizeof...(Args) / 2>{},
                        std::tuple{args...});   
}

auto test()
{
    apply_on_each_2_args(foo, 1, 2, 3, 4); // calls foo(1, 2) foo(3, 4)
}

Forwarding omitted for brevity.

To better understand how this works we can manually expand:

apply(on_each_2_args(foo, 1, 2, 3, 4))
↳ apply_on_2x_indexes(f, std::index_sequence<0, 1>{}, std::tuple{1, 2, 3, 4})
  ↳ (f(std::get<0 * 2>(t), std::get<0 * 2 + 1>(t)),  f(std::get<1 * 2>(t), std::get<1 * 2 + 1>(t)))
    (f(std::get<0>(t), std::get<1>(t)),  f(std::get<2>(t), std::get<3>(t)))
    (f(1, 2), f(3, 4))

Another approach:

One thing that I don't like in your call syntax

apply_on_each_2_args([] (auto x, auto y) { }, 1, 2, "hello",  "bye");

is that is not clear how arguments are grouped per call.

So I would like to group them. Unfortunately I can't get it to work like this for varargs:

apply_on_each_2_args([] (auto x, auto y) { }, {1, 2}, {"hello",  "bye"});

but we can be a little more verbose with tuple:

template<class Func, class... Args>
void apply_on_each_2_args(Func f, Args... args)
{
    (std::apply(f, args), ...);
}

auto test()
{
    apply_on_each_2_args([](auto a, auto b){ /*use a, b*/ },
                         std::tuple{1, 2}, std::tuple{"hello", "bye"});
}

Is not exactly what you asked but is an approach worth considering.

like image 28
bolov Avatar answered Nov 14 '22 08:11

bolov