如何生成与函数的元数相同数量的参数?

22
假设我有以下函数,该函数将一个函数作为参数。
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!
}

使用方法如下:

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

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

就像foobar一样,我有几个返回T的函数,并且需要一些类型为T的参数。我希望test_func调用尽可能多的随机数生成器来匹配函数f所需的参数数量。换句话说,我们可以假设T始终是整数类型,并且每个参数都相同,即对随机数生成器进行函数调用。
使用function_traits(例如Boost中的那些),我可以获取F的返回类型,这有点帮助。大致上,我的问题是:
如何生成所需数量的函数调用,使其与函数F的元数匹配?
在C++11之前,我会看看Boost.Preprocessor,或者可能依赖于模板特化。现在有更好的方法吗?

6
你可以使用可变参数模板,并转发参数包。 - πάντα ῥεῖ
@Samuel 为了简单起见,我们可以假设每个参数都是对(比如)RNG的调用,就像例子中那样。此外,请假设“T”始终为整数类型。 - Juho
@πάνταῥεῖ 这怎么可能行得通呢?test_func不能打包参数,因为它们已经被明确定义为(int, int),将它们转发不会产生任意参数函数所需的正确数量的参数。 - Samuel
1
通常情况下,我只会提到,在C++中的可调用对象不一定具有明确定义的arity(参数个数)。 - R. Martinho Fernandes
模板不是函数。 - R. Martinho Fernandes
显示剩余7条评论
2个回答

22

首先定义一个名为arity的元函数来计算函数的元数(它只是一个简单的实现;可以改进为计算functor的元数。请参见我在这里的答案。):

template<typename F> 
struct arity;

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

接下来定义另一个元函数,名为genseq,用于生成编译时整数值的序列:

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!

然后一个函数调用器如下:

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] ... );
}

然后你的代码将变成这样:
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);
}

这里有一个样例演示

希望这能帮到您。


你知道吗?你对 arg_evaluator 的调用没有指定的评估顺序。 - Xeo
@Xeo:是的。:-)。就像问题中描述的那样。 - Nawaz
@Xeo:因为在问题中,OP正在使用“随机”生成器。你调用它的顺序真的很重要吗? - Nawaz
是的:不同的运行可能会产生不同的顺序。没有固定的种子会产生可预测的参数,有利于重现测试误差运行。 - Yakk - Adam Nevraumont
顺便提一下,上述代码在重载时会失败。有一个解决方案可以返回最少数量的参数。此外,上述代码也无法处理 void foo(int, double, int),但这也可以通过兼容3个double或3个int等方式来解决。很可能OP并不关心这些。 - Yakk - Adam Nevraumont
1
@Yakk:它会以许多其他方式失败。问题是:是否需要这样做?我在问题中没有看到任何这样的要求。无论如何,“未指定的评估顺序”问题已经解决了。 - Nawaz

10

不需要复杂的元数据计算。

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);
}
为了支持函数对象,需要稍微复杂一些的实现,但不会太难:
// 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());
}

好的解决方案。但请注意,它需要更改函数签名! - Nawaz
是的。但是对于函数对象的重载需要进行“复杂的元计算”,而这正是您一开始想要避免的。它不再那么简单了。 :-) - Nawaz
@Morwenn:“&F::operator()” 可能因为模棱两可而被编译器拒绝。这正是正确的做法。你会选择哪个 operator() - n. m.
2
小心啊,你中了参数未指定的评估顺序的陷阱。 - R. Martinho Fernandes
1
@R.MartinhoFernandes 不确定在哪里,请您指出具体行数? - n. m.
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接