解包元组以调用匹配的函数指针

289

我想在一个std::tuple中存储可变数量的值,这些值稍后将用作匹配存储类型的函数指针的参数。

我创建了一个简化的示例来展示我正在努力解决的问题:

#include <iostream>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template <typename ...Args>
struct save_it_for_later {
  std::tuple<Args...> params;
  void (*func)(Args...);

  void delayed_dispatch() {
     // How can I "unpack" params to call func?
     func(std::get<0>(params), std::get<1>(params), std::get<2>(params));
     // But I *really* don't want to write 20 versions of dispatch so I'd rather 
     // write something like:
     func(params...); // Not legal
  }
};

int main() {
  int a=666;
  double b = -1.234;
  void *c = NULL;

  save_it_for_later<int,double,void*> saved = {
                                 std::tuple<int,double,void*>(a,b,c), f};
  saved.delayed_dispatch();
}

通常在涉及到 std::tuple 或可变参数模板的问题时,我会编写另一个模板,例如 template <typename Head, typename ...Tail>,以递归地逐个评估所有类型,但我无法想到一种方法来调度函数调用。
实际动机有些复杂,主要是为了学习而已。您可以假设我从另一个接口通过契约获得了元组,因此不能更改,但希望将其解包成函数调用。这排除了使用 std::bind 作为绕过根本问题的廉价方法。
有什么干净利落的方法可以使用 std::tuple 调度调用,或者实现存储/转发某些值和函数指针直到任意未来时间点的另一种更好的方式?

5
为什么你不能只使用auto saved = std::bind(f, a, b, c);,然后稍后调用saved()呢? - Charles Salvia
不总是由我控制接口。我通过合约从其他人那里接收元组,并希望随后对其进行操作。 - Flexo
10个回答

291
你需要构建一个数字参数包并解包它们。
template<int ...>
struct seq { };

template<int N, int ...S>
struct gens : gens<N-1, N-1, S...> { };

template<int ...S>
struct gens<0, S...> {
  typedef seq<S...> type;
};


// ...
  void delayed_dispatch() {
     callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  void callFunc(seq<S...>) {
     func(std::get<S>(params) ...);
  }
// ...

5
哇,我不知道展开运算符可以这样使用,太好了! - Luc Touraille
6
约翰内斯,我知道你发表这篇文章已经两年多了,但我现在遇到的问题是struct gens的泛型定义(它继承自相同的扩展导出类型)。我看到最终它会使用0来进行特化。如果你有心情和空闲时间的话,能否详细解释一下它以及它如何被用于这个问题,我将非常感激。我希望我可以给这篇文章点赞一百次。我从这段代码中玩得很开心。谢谢。 - WhozCraig
23
它的作用是生成一个类型 seq<0, 1, .., N-1>。它的工作原理是:gens<5>: gens<4, 4>: gens<3, 3, 4>: gens<2, 2, 3, 4> : gens<1, 1, 2, 3, 4> : gens<0, 0, 1, 2, 3, 4>。最后一个类型是专门创建的,生成了 seq<0, 1, 2, 3, 4>。这是一种相当巧妙的技巧。 - mindvirus
2
@NirFriedman:当然,只需将gens的非特化版本替换为:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; }; - marton78
11
值得强调的是,沃尔特的回答和评论是有道理的:人们不需要再发明自己的轮子了。生成序列是如此普遍,以至于在C++14中将其标准化为std::integer_sequence<T, N>及其专门化版本std::size_tstd::index_sequence<N> - 再加上它们的相关辅助函数std::make_in(teger|dex)_sequence<>()std::index_sequence_for<Ts...>()。而在C++17中,库中集成了许多其他好东西,尤其包括std::applystd::make_from_tuple,它们会处理解包和调用部分。 - underscore_d
显示剩余6条评论

117

C++17的解决方案是简单地使用std::apply

auto f = [](int a, double b, std::string c) { std::cout<<a<<" "<<b<<" "<<c<< std::endl; };
auto params = std::make_tuple(1,2.0,"Hello");
std::apply(f, params);

在这个帖子中,我觉得应该再次提到一下(在其中一个评论中已经出现过了)。


这个帖子中还缺少基本的C++14解决方案。编辑:不,实际上在Walter的答案中已经有了。

给定这个函数:

void f(int a, double b, void* c)
{
      std::cout << a << ":" << b << ":" << c << std::endl;
}

使用以下片段进行调用:
template<typename Function, typename Tuple, size_t ... I>
auto call(Function f, Tuple t, std::index_sequence<I ...>)
{
     return f(std::get<I>(t) ...);
}

template<typename Function, typename Tuple>
auto call(Function f, Tuple t)
{
    static constexpr auto size = std::tuple_size<Tuple>::value;
    return call(f, t, std::make_index_sequence<size>{});
}

例子:

int main()
{
    std::tuple<int, double, int*> t;
    //or std::array<int, 3> t;
    //or std::pair<int, double> t;
    call(f, t);    
}

DEMO


我无法使用智能指针使这个演示程序正常工作 - 这里有什么问题? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb - Xeverous
谢谢,我有两个问题:1. 为什么我不能直接传递 std::make_unique?它需要具体的函数实例吗?2. 如果我们可以将 [](auto... ts) 更改为 [](auto&&... ts),为什么要使用 std::move(ts)... - Xeverous
@Xeverous:1. 从签名上看无法工作:你的 std::make_unique 需要一个元组,而一个元组只能通过另一个调用 std::make_tuple 来创建。这就是我在 lambda 中所做的(虽然这非常冗余,因为你也可以简单地将元组复制到唯一指针中,而不需要使用 call)。 - davidhigh
@Xeverous:2. 你可以选择第二种方式,但我猜你想要使用副本进行操作...几乎没有必要拥有指针或引用。 (你也可以通过使用类似于std::make_tuple<std::decay_t<decltype(ts)...>>(std::forward<decltype(ts)>(ts) ...)的返回值来实现第二个版本,但这需要写更多的代码。) - davidhigh
1
这现在应该是正确的答案。 - Fureeish
显示剩余2条评论

46

这是Johannes的解决方案的完整可编译版本,用于回答awoodland的问题,希望能对某些人有所帮助。本代码是在Debian squeeze上使用g++ 4.7快照测试通过的。

###################
johannes.cc
###################
#include <tuple>
#include <iostream>
using std::cout;
using std::endl;

template<int ...> struct seq {};

template<int N, int ...S> struct gens : gens<N-1, N-1, S...> {};

template<int ...S> struct gens<0, S...>{ typedef seq<S...> type; };

double foo(int x, float y, double z)
{
  return x + y + z;
}

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  double (*func)(Args...);

  double delayed_dispatch()
  {
    return callFunc(typename gens<sizeof...(Args)>::type());
  }

  template<int ...S>
  double callFunc(seq<S...>)
  {
    return func(std::get<S>(params) ...);
  }
};

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
int main(void)
{
  gens<10> g;
  gens<10>::type s;
  std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
  save_it_for_later<int,float, double> saved = {t, foo};
  cout << saved.delayed_dispatch() << endl;
}
#pragma GCC diagnostic pop

以下是可用的SConstruct文件:

#####################
SConstruct
#####################
#!/usr/bin/python

env = Environment(CXX="g++-4.7", CXXFLAGS="-Wall -Werror -g -O3 -std=c++11")
env.Program(target="johannes", source=["johannes.cc"])

在我的机器上,这将会得到以下结果。
g++-4.7 -o johannes.o -c -Wall -Werror -g -O3 -std=c++11 johannes.cc
g++-4.7 -o johannes johannes.o

你为什么需要变量s和g? - shoosh
@shoosh 我想它们不是必需的。我忘记了为什么要添加它们,已经过去将近三年了。但我认为,这样可以展示实例化起作用了。 - Faheem Mitha

43

下面是一个 C++14 的解决方案。

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  void (*func)(Args...);

  template<std::size_t ...I>
  void call_func(std::index_sequence<I...>)
  { func(std::get<I>(params)...); }
  void delayed_dispatch()
  { call_func(std::index_sequence_for<Args...>{}); }
};

这仍需一个辅助函数 (call_func)。由于这是一个常见的用法,或许标准库应该直接支持它并命名为 std::call,并提供可能的实现。

// helper class
template<typename R, template<typename...> class Params, typename... Args, std::size_t... I>
R call_helper(std::function<R(Args...)> const&func, Params<Args...> const&params, std::index_sequence<I...>)
{ return func(std::get<I>(params)...); }

// "return func(params...)"
template<typename R, template<typename...> class Params, typename... Args>
R call(std::function<R(Args...)> const&func, Params<Args...> const&params)
{ return call_helper(func,params,std::index_sequence_for<Args...>{}); }

然后我们的延迟派发变成了

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  std::function<void(Args...)> func;
  void delayed_dispatch()
  { std::call(func,params); }
};

8
因为Walter被迫使用笨重的语法std::index_sequence_for<Args...>{},所以请注意在这里解释了C++14的混乱动物园中的integer_sequenceindex_sequence辅助类型:http://en.cppreference.com/w/cpp/utility/integer_sequence,并因(拟议的)实现`std :: call而受到赞赏。请注意缺少std::make_index_sequence(Args...)`的明显缺席。 - Quuxplusone
3
据说自2016年3月以来被投票纳入C++17标准库中,用法为std::apply(func, tup)。具体内容可参考http://en.cppreference.com/w/cpp/utility/apply。 - ddevienne

18

实现这个功能有点复杂(虽然是可能的)。我建议您使用一个已经实现了这个功能的库,即Boost.Fusion(使用invoke函数)。作为额外的奖励,Boost Fusion也与C++03编译器兼容。


8

解决方案。首先,一些实用的样板:

template<std::size_t...Is>
auto index_over(std::index_sequence<Is...>){
  return [](auto&&f)->decltype(auto){
    return decltype(f)(f)( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto index_upto(std::integral_constant<std::size_t, N> ={}){
  return index_over( std::make_index_sequence<N>{} );
}

这些让您使用一系列编译时整数调用lambda函数。
void delayed_dispatch() {
  auto indexer = index_upto<sizeof...(Args)>();
  indexer([&](auto...Is){
    func(std::get<Is>(params)...);
  });
}

我们完成了。

index_uptoindex_over 让你能够在不生成新的外部重载的情况下处理参数包。

当然,在 中,你只需要

void delayed_dispatch() {
  std::apply( func, params );
}

现在,如果我们喜欢,在中我们可以写:

namespace notstd {
  template<class T>
  constexpr auto tuple_size_v = std::tuple_size<T>::value;
  template<class F, class Tuple>
  decltype(auto) apply( F&& f, Tuple&& tup ) {
    auto indexer = index_upto<
      tuple_size_v<std::remove_reference_t<Tuple>>
    >();
    return indexer(
      [&](auto...Is)->decltype(auto) {
        return std::forward<F>(f)(
          std::get<Is>(std::forward<Tuple>(tup))...
        );
      }
    );
  }
}

相对容易地,您可以准备好更加简洁的 语法以便发布。
void delayed_dispatch() {
  notstd::apply( func, params );
}

当你的编译器升级时,只需用std替换notstd,然后一切就搞定了。


std::apply <- 音乐听起来很动听 - Flexo
@Flexo 只比 index_upto 稍微短一点,也不太灵活。;) 尝试使用 index_uptostd::apply 分别反向调用 func 的参数。诚然,谁会想要从元组中反向调用函数呢。 - Yakk - Adam Nevraumont
小细节:std::tuple_size_v 是 C++17 的特性,所以对于 C++14 的解决方案,需要将其替换为 typename std::tuple_size<foo>::value - basteln
@basteln 希望 value 不是一个类型。但无论如何已修复。 - Yakk - Adam Nevraumont
@adri 是一个(std)整数常量,它具有一个constexpr操作符size_t,即使在非constexpr的*this情况下也可以工作。 - Yakk - Adam Nevraumont
显示剩余2条评论

3

根据给出的答案,我进一步思考了这个问题,发现了另一种解决方案:

template <int N, int M, typename D>
struct call_or_recurse;

template <typename ...Types>
struct dispatcher {
  template <typename F, typename ...Args>
  static void impl(F f, const std::tuple<Types...>& params, Args... args) {
     call_or_recurse<sizeof...(Args), sizeof...(Types), dispatcher<Types...> >::call(f, params, args...);
  }
};

template <int N, int M, typename D>
struct call_or_recurse {
  // recurse again
  template <typename F, typename T, typename ...Args>
  static void call(F f, const T& t, Args... args) {
     D::template impl(f, t, std::get<M-(N+1)>(t), args...);
  }
};

template <int N, typename D>
struct call_or_recurse<N,N,D> {
  // do the call
  template <typename F, typename T, typename ...Args>
  static void call(F f, const T&, Args... args) {
     f(args...);
  }
};

需要更改delayed_dispatch()的实现方式为:

  void delayed_dispatch() {
     dispatcher<Args...>::impl(func, params);
  }

这个方法通过将std::tuple递归转换成独立的参数包来实现。需要使用call_or_recurse作为特例来终止递归,真正的调用只是展开完成的参数包。
我不确定这是否以任何方式都是“更好”的解决方案,但这是另一种思考和解决它的方式。
另一个替代解决方案是使用enable_if,形成比我先前的解决方案可能更简单的东西:
#include <iostream>
#include <functional>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template <typename ...Args>
struct save_it_for_later {
  std::tuple<Args...> params;
  void (*func)(Args...);

  template <typename ...Actual>
  typename std::enable_if<sizeof...(Actual) != sizeof...(Args)>::type
  delayed_dispatch(Actual&& ...a) {
    delayed_dispatch(std::forward<Actual>(a)..., std::get<sizeof...(Actual)>(params));
  }

  void delayed_dispatch(Args ...args) {
    func(args...);
  }
};

int main() {
  int a=666;
  double b = -1.234;
  void *c = NULL;

  save_it_for_later<int,double,void*> saved = {
                                 std::tuple<int,double,void*>(a,b,c), f};
  saved.delayed_dispatch();
}

第一个重载函数从元组中取出一个参数并将其放入参数包中。第二个重载函数需要一个匹配的参数包,然后进行真正的调用,在唯一情况下禁用第一个重载函数而启用第二个重载函数。


1
我之前曾经做过一个非常类似的项目。如果我有时间,我会再去看一眼,看看它与当前答案的比较情况如何。 - Michael Price
@MichaelPrice - 纯从学习角度来看,我很想看到任何不归结于某些可怕的黑客攻击堆栈指针(或类似地调用约定特定技巧)的替代解决方案。 - Flexo

2

我采用了Johannes的解决方案,使用了C++14标准库中的std::index_sequence (并将函数返回类型作为模板参数RetT):

template <typename RetT, typename ...Args>
struct save_it_for_later
{
    RetT (*func)(Args...);
    std::tuple<Args...> params;

    save_it_for_later(RetT (*f)(Args...), std::tuple<Args...> par) : func { f }, params { par } {}

    RetT delayed_dispatch()
    {
        return callFunc(std::index_sequence_for<Args...>{});
    }

    template<std::size_t... Is>
    RetT callFunc(std::index_sequence<Is...>)
    {
        return func(std::get<Is>(params) ...);
    }
};

double foo(int x, float y, double z)
{
  return x + y + z;
}

int testTuple(void)
{
  std::tuple<int, float, double> t = std::make_tuple(1, 1.2, 5);
  save_it_for_later<double, int, float, double> saved (&foo, t);
  cout << saved.delayed_dispatch() << endl;
  return 0;
}

所有这些解决方案可能会解决最初的问题,但老实说,这个模板东西是否朝着简单性和可维护性的错误方向发展? - x y
我认为随着C++11和14的出现,模板变得更好、更易于理解。几年前,当我看到boost在模板下面做了什么时,我感到非常沮丧。我同意开发良好的模板比仅仅使用它们要困难得多。 - schwart
2
@xy 首先,就模板复杂度而言,这根本算不上什么。其次,大多数辅助模板都是一项最初的投资,但在以后实例化时可以节省大量时间。最后,你难道不想拥有使用模板所允许的功能吗?你可以选择不使用它,并且不留下似乎在监管其他程序员的无关评论。 - underscore_d

0
提供了很多答案,但我觉得它们太复杂了,不是很自然。我用另一种方式实现了它,而没有使用sizeof或计数器。我使用了自己简单的结构(ParameterPack)来作为参数访问参数的尾部,而不是元组。然后,我将我的结构中的所有参数附加到函数参数中,最后,在不需要解包更多参数时,运行函数。这是C++11代码,我同意与其他答案相比,代码更多,但我发现它更易于理解。
template <class ...Args>
struct PackParameters;

template <>
struct PackParameters <>
{
    PackParameters() = default;
};

template <class T, class ...Args>
struct PackParameters <T, Args...>
{
    PackParameters ( T firstElem, Args... args ) : value ( firstElem ), 
    rest ( args... ) {}

    T value;
    PackParameters<Args...> rest;
};

template <class ...Args>
struct RunFunction;

template <class T, class ...Args>
struct RunFunction<T, Args...>
{
    template <class Function>
    static void Run ( Function f, const PackParameters<T, Args...>& args );

    template <class Function, class... AccumulatedArgs>
    static void RunChild ( 
                          Function f, 
                          const PackParameters<T, Args...>& remainingParams, 
                          AccumulatedArgs... args 
                         );
};

template <class T, class ...Args>
template <class Function>
void RunFunction<T, Args...>::Run ( 
                                   Function f, 
                                   const PackParameters<T, Args...>& remainingParams 
                                  )
{
    RunFunction<Args...>::template RunChild ( f, remainingParams.rest,
                                              remainingParams.value );
}

template <class T, class ...Args>
template<class Function, class ...AccumulatedArgs>
void RunFunction<T, Args...>::RunChild ( Function f, 
                                         const PackParameters<T, Args...>& remainingParams, 
                                         AccumulatedArgs... args )
{
    RunFunction<Args...>:: template RunChild ( f, remainingParams.rest,
                                               args..., remainingParams.value );
}


template <>
struct RunFunction<>
{
    template <class Function, class... AccumulatedArgs>
    static void RunChild ( Function f, PackParameters<>, AccumulatedArgs... args )
    {
        f ( args... );
    }

    template <class Function>
    static void Run ( Function f, PackParameters<> )
    {
        f ();
    }
};

struct Toto
{
    std::string k = "I am toto";
};

void f ( int i, Toto t, float b, std::string introMessage )
{
    float res = i * b;

    std::cerr << introMessage << " " << res << std::endl;
    std::cerr << "Toto " << t.k << std::endl;
}

int main(){
    Toto t;
    PackParameters<int, Toto, float, std::string> pack ( 3, t, 4.0, " 3 * 4 =" );

    RunFunction<int, Toto, float, std::string>::Run ( f, pack );
    return 0;
}

0

最近我不得不处理这个问题,并发现这里的讨论很有用,但是对于我的用例使用函数符有点繁琐。希望我的解决方案能够使后来者受益。

#include <iostream>
#include <tuple>

void f(int a, double b, void* c) {
  std::cout << a << ":" << b << ":" << c << std::endl;
}

template<void func(int, double, void*)>
struct foo {
    
    template<class... args_t>
    constexpr void operator()(args_t... args) {
      func(args...);
    }

    template<class... T, size_t... I>
    constexpr void operator()(std::tuple<T...>& t, std::index_sequence<I...>) {
      this->operator()(std::get<I>(std::forward<std::tuple<T...>>(t))...);
    }

    template<typename... T>
    constexpr void operator()(std::tuple<T...>&& t)
    {        
      this->operator()(t, std::index_sequence_for<T...>{});
    }
};

int main() {
  foo<&f>()(std::make_tuple(1, 45.2, nullptr));
  return 0;
}

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