7

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?

max66
  • 65,235
  • 10
  • 71
  • 111
lrleon
  • 2,610
  • 3
  • 25
  • 38
  • What if `f` has default arguments? – xskxzr Jun 23 '18 at 19:04
  • Do you want consume one or two args for call? I mean: given `apply_on_each_args(lambda, 1, 2, "hello", "bye")` do you expect to call `lambda(1, 2), lambda(2, "hello"), lambda("hello", "bye")` or only `lambda(1, 2), lambda("hello", "bye")`? – max66 Jun 23 '18 at 19:32
  • @max66 I would like to consume the args is groups according to the number of arguments that the lambda expects. that is: `lambda(1, 2)`, next `lambda("hello", "bye")`. Thanks – lrleon Jun 23 '18 at 19:38
  • And you want a function that accept a lambda with an undefined number of arguments? – max66 Jun 23 '18 at 19:41
  • yes, if possible. But I would not have any problem with a function expecting lambdas with 2, 3... etc. Some like such `apply_on_each_2_args(lambda, 1, 2, "hello", "bye")`, `apply_on_each_3_args(lambda, 1, 2, 3, "hello", "intermezzo", "bye")`, – lrleon Jun 23 '18 at 19:43
  • with a fixed number of arguments, if you accept the classic recursive solution, it's simple. Do you accept a recursive solution or do you want a C++17 unpacking style solution? – max66 Jun 23 '18 at 19:45
  • Any kind of solution would be good for me, although I am curious about the c++17 version. – lrleon Jun 23 '18 at 19:48
  • 1
    So do I: I don't know a way to make it in C++17 style; I'll prepare you a simple recursive with fixed number of arguments; if something better comes in my mind, I'll try to develop it. – max66 Jun 23 '18 at 19:51

3 Answers3

4

The best I can imagine (at the moment) is the good old recursive way.

By example

// ground case
template <typename Func>
void apply_on_each_2_args (Func)
 { }

// recursive case
template <typename Func, typename A0, typename A1, typename ... Args>
void apply_on_each_2_args (Func f, A0 a0, A1 a1, Args ... args)
 { f(a0, a1); apply_on_each_2_args(f, args...); }
max66
  • 65,235
  • 10
  • 71
  • 111
  • There would be a base case missing; for stopping recursion and it allows the compiler to generate. Right? – lrleon Jun 23 '18 at 20:09
  • @Irleon - you're obviously right; I was thinking an alternative way and I've forgotten the ground case; sorry; answer corrected. – max66 Jun 23 '18 at 20:17
3

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.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • 2
    The `(func, {1, 2}, {"hello", "bye"})` syntax could work if the template function takes `std::initializer_list` parameters, but unfortunately that means all arguments in each group would need to have the same type. – aschepler Jun 23 '18 at 21:22
  • Nice voodoo. I've copie... ehmm... I've taken inspiration from it. – max66 Jun 24 '18 at 01:32
2

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);
 }
max66
  • 65,235
  • 10
  • 71
  • 111