22

Suppose I have the following function, that takes a function as a parameter.

template <typename F>
void test_func(F f)
{
    // typedef typename function_traits<F>::return_type T;
    typedef int T;

    std::mt19937 rng(std::time(0));
    std::uniform_int_distribution<T> uint_dist10(0, std::numeric_limits<T>::max());

    f(uint_dist10(rng), uint_dist10(rng)); // Problem!
}

Usage would be:

int foo(int, int) { return 0; }
int bar(int, int, int, int) { return 0; }

int main()
{
    test_func(foo);
    // test_func(bar);
}

Just like foo and bar, I have several functions that return T, and take some amount of parameters of type T. I would like test_func to generate as many calls to my RNG as the function f takes parameters. In other words, we can assume T is always an integer type, and that each parameter will be the same, i.e. a function call to an RNG.

Using function_traits (such as the ones in Boost), I can fetch the return type of F, and that helps a little. Roughly, my question is

How can I generate a needed amount of function calls so that it matches the arity of the function F?

Before C++11, I would have looked at Boost.Preprocessor, or maybe relied on template specialization. Is there a nicer way of doing it now?

Juho
  • 976
  • 1
  • 13
  • 27
  • Well the arity is a problem indeed. If you pass bar into it, what should the other arguments be? How should anybody except you being able to select proper values? The compiler surely cannot. A workaround is wrapping the call in a lambda or another function that has the knowledge of the remaining arguments. – Samuel May 14 '14 at 08:36
  • 6
    You could use a variadic template instead and forward the parameter pack. – πάντα ῥεῖ May 14 '14 at 08:37
  • @Samuel For simplicity, we can assume each argument is a call to (say) an RNG, just as in the example. Furthermore, assume `T` will always be an integer type. – Juho May 14 '14 at 08:37
  • @πάνταῥεῖ how would that work? The test_func cannot pack parameters as they are explicitly defined being (int, int) forwarding them would not yield in the correct amount of params for functions with arbitrary params. – Samuel May 14 '14 at 08:39
  • Related: http://stackoverflow.com/questions/3351056/create-va-list-dynamically –  May 14 '14 at 08:51
  • How about checking the arity of the passed function, then generating a variadic template list of arguments recursively, in the caller function, once done, then applying this to the passed function via a variadic blast? - This is how I would do it. For checking arity and the type of the n'th parameter simply use the function traits library, that was with my proposal to the ISO committee. – Skeen May 14 '14 at 08:56
  • @DieterLücking: Not really; va_lists are runtime constructs and this is compile time. – MSalters May 14 '14 at 10:00
  • 1
    As I usually do, I'll just mention that callable objects in C++ don't necessarily have a well-defined arity. – R. Martinho Fernandes May 14 '14 at 11:10
  • @R.MartinhoFernandes: Not even functions have well-defined arity. Variadic (template) functions for example. – Nawaz May 14 '14 at 11:13
  • Templates are not functions. – R. Martinho Fernandes May 14 '14 at 11:14
  • @Nawaz: They're called _function templates_, and not _template functions_ for a reason. – Xeo May 14 '14 at 11:15
  • @Xeo: Yes. I know it is called *function templates*. Please read *"Variadic (template) functions"* as "variadic functions" and "variadic function template". – Nawaz May 14 '14 at 11:32

2 Answers2

22

First define a meta function called arity to compute arity of the function (it is just a simple implementation; can be improved to compute arity of functors also. See my answer here.):

template<typename F> 
struct arity;

template<typename R, typename ...Args> 
struct arity<R (*)(Args...)>
{
    static const std::size_t value = sizeof ... (Args);
};

then define another meta function called genseq to generate a compile time sequence of integral values:

template<int ... N>
struct seq
{
    using type = seq<N...>;

    template<int I>
    struct push_back : seq<N..., I> {};
};

template<int N>
struct genseq : genseq<N-1>::type::template push_back<N-1> {};

template<>
struct genseq<0> : seq<> {};

template<int N>
using genseq_t = typename genseq<N>::type;  //Just a friendly alias!

then a function invoker as:

template<typename F, typename ArgEvaluator, int ...N>
void invoke(seq<N...>, F f, ArgEvaluator arg_evaluator)
{
    using arg_type = decltype(arg_evaluator());

    constexpr std::size_t arity = sizeof ... (N);

    arg_type args[] { (N, arg_evaluator()) ... }; //enforce order of evaluation

    f( args[N] ... );
}

And then your code would become this:

template <typename F>
void test_func(F f)
{
    // typedef typename function_traits<F>::return_type T;
    typedef int T;

    std::mt19937 rng(std::time(0));
    std::uniform_int_distribution<T> uint_dist10(0, std::numeric_limits<T>::max());

    //f(uint_dist10(rng), uint_dist10(rng)); // Problem!

      auto arg_evaluator = [&]() mutable { return uint_dist10(rng); };
      invoke(genseq_t<arity<F>::value>(), f, arg_evaluator);
}

Here is a sample demo.

Hope that helps.

Community
  • 1
  • 1
Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • Did you know your invocations of `arg_evaluator` have unspecified evaluation order? – Xeo May 14 '14 at 11:13
  • @Xeo: Yes. :-). It is same as in the question. – Nawaz May 14 '14 at 11:14
  • @Xeo: Because in the question, the OP is using "random" generator. Does it really matter the order in which you're calling it? – Nawaz May 14 '14 at 11:21
  • Yes: different runs could evsluate in different orders. Without that a fixed seed produces predicable parameters, good for reproducing the test error run. – Yakk - Adam Nevraumont May 14 '14 at 11:40
  • As an aside, the above fails on overloads. There is a solution that returns the lest number of args. Also, the above fails on `void foo(int, double, int)`, which can also be solved to be compatible with 3 `double` or 3 `int` or the like. Odds are the OP does not care. – Yakk - Adam Nevraumont May 14 '14 at 11:46
  • 1
    @Yakk: It will fail in many other ways. The question is: is it required to do that? I don't see any such requirement in the question. Anyway, the "unspecified evaluation order" problem is fixed. – Nawaz May 14 '14 at 12:01
10

No need for complicated meta calculations.

template <typename Ret, typename ... T>
void test_func (Ret f (T...))
{
  std::mt19937 rng(std::time(0));
  f((std::uniform_int_distribution<T>(0, std::numeric_limits<T>::max())(rng))...);
}

int moo(int, int, int){ return 0; }

int main ()
{
  test_func(moo);
}

To support functors one needs a bit longer implementation, still not too complicated:

// separate arguments type from function/functor type
template <typename F, typename ... T> 
void test_func_impl (F f)
{
  std::mt19937 rng(std::time(0));
  f((std::uniform_int_distribution<T>(0, std::numeric_limits<T>::max())(rng))...);
}

// overload for a straight function
template <typename Ret, typename ... T>
void test_func (Ret f (T...))
{
  test_func_impl<decltype(f), T...>(f);
}

// forwarder for a functor with a normal operator()
template <typename F, typename Ret, typename... T>
void test_func_for_functor (F f, Ret (F::*)(T...))
{
  test_func_impl<F, T...>(f);
}

// forwarder for a functor with a const operator()
template <typename F, typename Ret, typename... T>
void test_func_for_functor (F f, Ret (F::*)(T...)const)
{
  test_func_impl<F, T...>(f);
}

// overload for anything that has operator()
template <typename F>
void test_func (F f)
{
  test_func_for_functor(f, &F::operator());
}
n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Good solution. But note that it requires the function signature to change! – Nawaz May 14 '14 at 09:02
  • @Nawaz Only the template arguments are different. It rarely matters. You can always wrap it in a `template void correct_func(F f)` if desired. – n. m. could be an AI May 14 '14 at 09:10
  • What I mean is that if the caller uses functor instead of function, this wouldn't work. Mine (in the current form) wouldn't work either. But as I said in the answer itself, it all needs a more sophisticated implementation of `arity` meta function. All else will remain same. I also provided a link to my other answer which give some idea as to how to implement `arity` to work with functors. – Nawaz May 14 '14 at 09:25
  • I see what you mean. It's easy to provide an overload for functors too. Will update the answer soon. – n. m. could be an AI May 14 '14 at 09:57
  • Yes. But overloading for functors require *"complicated meta calculations"* which you wanted to avoid in the first place. It wouldn't be this simple anymore. :-) – Nawaz May 14 '14 at 10:01
  • Note that the number of overloads needed is quite horrible if you also need to support all kind of funciton cv/ref qualifiers. – Morwenn May 14 '14 at 10:40
  • @Morwenn I have never seen "v" member functions used in practice, only "c", so I won't bother. Wouldn't you need the same number of overloads to implement `arity` from the other solution? – n. m. could be an AI May 14 '14 at 10:43
  • Also, I am not sure, but a result of `std::bind` may have multiple overloads of `operator()` which means that `&F::operator()` may be rejected by the compiler for being ambiguous. – Morwenn May 14 '14 at 10:43
  • @n.m. That's totally right. To get around the problem, all the boilerplate is generally written in `function_traits` (which can be reused). – Morwenn May 14 '14 at 10:44
  • @Morwenn "&F::operator() may be rejected by the compiler for being ambiguous". That's exactly the right thing to do. Which `operator()` would *you* select? – n. m. could be an AI May 14 '14 at 10:48
  • @Morwenn You can wrap this method into a reusable package too. – n. m. could be an AI May 14 '14 at 10:56
  • 2
    Careful there. You fell prey to the unspecified evaluation order of arguments. – R. Martinho Fernandes May 14 '14 at 11:13
  • 1
    @R.MartinhoFernandes Not sure where, can you point out the line? – n. m. could be an AI May 14 '14 at 11:32
  • @n.m. I don't know, but I wouldn't have expected multiple overloads of `operator()` by a result of `std::bind` before running into the problem. – Morwenn May 14 '14 at 11:59