Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I bind a ::std::vector of arguments to a functor?

I'm trying to make this program compile properly:

#include <vector>
#include <iostream>

int f(int a, int b)
{
   ::std::cout << "f(" << a << ", " << b << ") == " << (a + b) << '\n';
   return a + b;
}

template <typename R, typename V>
R bind_vec(R (*f)(), const V &vec, int idx=0)
{
   return f();
}

template <typename R, typename V, typename Arg1, typename... ArgT>
R bind_vec(R (*f)(Arg1, ArgT...), const V &vec, int idx=0)
{
   const Arg1 &arg = vec[idx];
   auto call = [arg, f](ArgT... args) -> R {
      return (*f)(arg, args...);
   };
   return bind_vec(call, vec, idx+1);
}

int foo()
{
   ::std::vector<int> x = {1, 2};
   return bind_vec(f, x);
}

Ideally I'd like bind_vec to take an arbitrary functor as an argument instead of just a function pointer. The idea is to pull the function arguments from a ::std::vector at compile time.

This isn't the final use for this, but it's a stepping stone to where I want to go. What I'm really doing is generating wrapper functions that unwrap their arguments from promises in a future/promise type system at compile time. These wrapper functions will themselves be promises.

In my ultimate use-case I can count on the functors being ::std::functions. But it would be nice to have an idea of how it should work for more general functors as well since I think this is a broadly interesting problem.

like image 449
Omnifarious Avatar asked Dec 26 '22 13:12

Omnifarious


2 Answers

OK, first off, detecting the arity of a functor can be done, but it's a bit involved and best left to a separate question. Let's assume you will specify the arity of the functor in the call. Similarly, there are ways to obtain the return type of a callable object, but that's also beyond the scope of this question. Let's just assume the return type is void for now.

So we want to say,

call(F f, C v);

and that should say f(v[0], v[1], ..., v[n-1]), where f has arity n.


Here's an approach:

template <unsigned int N, typename Functor, typename Container>
void call(Functor const & f, Container const & c)
{
    call_helper<N == 0, Functor, Container, N>::engage(f, c);
}

We need the helper:

#include <functional>
#include <cassert>

template <bool Done, typename Functor, typename Container,
          unsigned int N, unsigned int ...I>
struct call_helper
{
    static void engage(Functor const & f, Container const & c)
    {
        call_helper<sizeof...(I) + 1 == N, Functor, Container,
                    N, I..., sizeof...(I)>::engage(f, c);
    }
};

template <typename Functor, typename Container,
          unsigned int N, unsigned int ...I>
struct call_helper<true, Functor, Container, N, I...>
{
    static void engage(Functor const & f, Container const & c)
    {
        assert(c.size() >= N);
        f(c[I]...);
    }
};

Example:

#include <vector>
#include <iostream>

void f(int a, int b) { std::cout << "You said: " << a << ", " << b << "\n"; }

struct Func
{
    void operator()(int a, int b) const
    { std::cout << "Functor: " << a << "::" << b << "\n"; }
};

int main()
{
    std::vector<int> v { 20, 30 };
    call<2>(f, v);
    call<2>(Func(), v);
}

Notes: In a more advanced version, I would deduce the arity of the callable object with some more template machinery, and I would also deduce the return type. For this to work, you'll need several specializations for free functions and various CV-qualified class member functions, though, and so this would be getting too large for this question.

like image 169
Kerrek SB Avatar answered Jan 10 '23 00:01

Kerrek SB


Something like this is easily possible for (member) function pointers, but for functors with potentially overloaded operator(), this gets a dang lot harder. If we assume that you have a way to tell how many arguments a function takes (and assume that the container actually has that many elements), you can just use the indices trick to expand the vector into an argument list, for example with std::next and a begin() iterator:

#include <utility>
#include <iterator>

template<class F, class Args, unsigned... Is>
auto invoke(F&& f, Args& cont, seq<Is...>)
  -> decltype(std::forward<F>(f)(*std::next(cont.begin(), Is)...))
{
  return std::forward<F>(f)(*std::next(cont.begin(), Is)...);
}

template<unsigned ArgC, class F, class Args>
auto invoke(F&& f, Args& cont)
  -> decltype(invoke(std::forward<F>(f), cont, gen_seq<ArgC>{}))
{
  return invoke(std::forward<F>(f), cont, gen_seq<ArgC>{});
}

This implementation works really nice for random-access containers, but not so well for forward and especially input ones. To make those work in a performant fashion, you might try to go the route of incrementing the iterator with every expanded step, but you'll run into a problem: Evaluation order of arguments to a function is unspecified, so you'll very likely pass the arguments in the wrong order.

Luckily, there is a way to force evaluation left-to-right: The list-initialization syntax. Now we just need a context where that can be used to pass arguments, and a possible one would be to construct an object, pass the function and the arguments through the constructor, and call the function in there. However, you lose the ability to retrieve the returned value, since constructors can't return a value.

Something I thought of is to create an array of iterators, which point to the correct element, and expanding those again in a second step where they are dereferenced.

#include <utility>

template<class T> using Alias = T; // for temporary arrays

template<class F, class It, unsigned N, unsigned... Is>
auto invoke_2(F&& f, It (&&args)[N], seq<Is...>)
  -> decltype(std::forward<F>(f)(*args[Is]...))
{
  return std::forward<F>(f)(*args[Is]...);
}

template<class F, class Args, unsigned... Is>
auto invoke_1(F&& f, Args& cont, seq<Is...> s)
  -> decltype(invoke_2(std::forward<F>(f), std::declval<decltype(cont.begin())[sizeof...(Is)]>(), s))
{
  auto it = cont.begin();
  return invoke_2(std::forward<F>(f), Alias<decltype(it)[]>{(void(Is), ++it)...}, s);
}

template<unsigned ArgC, class F, class Args>
auto invoke(F&& f, Args& cont)
  -> decltype(invoke_1(std::forward<F>(f), cont, gen_seq<ArgC>{}))
{
  return invoke_1(std::forward<F>(f), cont, gen_seq<ArgC>{});
}

The code was tested against GCC 4.7.2 and works as advertised.


Since you said that the functors you are getting passed are std::functions, getting the number of arguments they take is really easy:

template<class F> struct function_arity;

// if you have the 'Signature' of a 'std::function' handy
template<class R, class... Args>
struct function_arity<R(Args...)>
  : std::integral_constant<std::size_t, sizeof...(Args)>{};

// if you only have the 'std::function' available
template<class R, class... Args>
struct function_arity<std::function<R(Args...)>>
  : function_arity<R(Args...)>{};

Note that you don't even need function_arity to make invoke from above work for std::function:

template<class R, class... Ts, class Args>
R invoke(std::function<R(Ts...)> const& f, Args& cont){
  return invoke_1(f, cont, gen_seq<sizeof...(Ts)>{})
}
like image 42
Xeo Avatar answered Jan 10 '23 00:01

Xeo