“解包”一个元组以调用匹配的函数指针


253

我正在尝试存储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 2015年

并非总是我的界面来控制。我通过合同从其他人那里收到一个元组,然后想要处理它。
Flexo

Answers:


275

您需要构建数字参数包并解压它们

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

4
哇,我不知道可以像这样使用拆包运算符,太好了!
卢·图拉耶

5
Johannes,我知道距您发布此书已有2年多了,但我一直在努力的一件事是struct gens通用定义(该定义继承自 相同的扩展推导)。我看到它最终会达到0的专业化水平。如果心情适合您并且您有空闲的周期,如果您可以对此进行扩展,以及如何将其用于此方面,我将永远感激不已。我希望我可以投票一百次。使用此代码中的切线,我获得了更多的乐趣。谢谢。
WhozCraig 2013年

22
@WhozCraig:它所做的是生成一个type 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 2014年

2
@NirFriedman:当然,只需替换以下非专业版本即可genstemplate <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
值得在沃尔特(Walter)的回答和评论上回音:人们不再需要发明自己的轮子了。生成序列是如此普遍,以至于在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::applyand和std::make_from_tuple,它们可以
underscore_d

61

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解决方案。编辑:不,它实际上在沃尔特的答案中。

该函数给出:

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

演示


我无法使该演示与智能指针一起使用-这怎么了?http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous

@Xeverous:您想在这里得到类似的东西吗?
davidhigh'9

谢谢,我有两个问题:1.为什么我不能std::make_unique直接通过?是否需要具体的功能实例?2.为什么std::move(ts)...可以更改[](auto... ts)[](auto&&... ts)
Xeverous

@Xeverous:1.在签名中不起作用:您std::make_unique期望一个元组,并且只有通过另一个对的调用,才能从解压缩的元组中创建一个元组std::make_tuple。这就是我在lambda中所做的事情(尽管它是高度冗余的,因为您也可以将元组简单地复制到唯一的指针中,而无需使用call)。
davidhigh'9

1
现在应该答案。
Fureeish

44

这是约翰内斯对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 2015年

@shoosh我猜他们是不需要的。我忘记了为什么添加这些内容;已经快三年了。但是我想证明实例化是可行的。
Faheem Mitha 2015年

42

这是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
为的(建议的)实施支持std::call。C ++ 14的混乱integer_sequenceindex_sequence辅助类型在这里进行了解释:en.cppreference.com/w/cpp/utility/integer_sequence注意明显缺少std::make_index_sequence(Args...),这就是为什么Walter被迫使用笨拙的语法std::index_sequence_for<Args...>{}
Quuxplusone

3
2016
3


6

解。首先,一些实用模板:

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

只需在编译器升级时替换notstdstd,而bob是您的叔叔。


std::apply<-音乐在我耳中
柔印

@Flexo只是比它短index_upto而不太灵活。;)尝试分别func使用index_upto和向后调用参数std::apply。诚然,到底谁想从元组中向后调用函数。
Yakk-亚当·内夫罗蒙特

次要点:std::tuple_size_v是C ++ 17,因此对于C ++ 14解决方案,必须将其替换为typename std::tuple_size<foo>::value
basteln

@basteln我希望value不是一种类型。但是无论如何都解决了。
Yakk-Adam Nevraumont

@Yakk不,是sizeof...(Types)。我喜欢没有的解决方案typename
basteln

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-纯粹从学习的角度来看,我很感兴趣的是看到任何其他解决方案都不能归结为一些令人讨厌的技巧,它们会破坏堆栈指针(或类似地调用约定的特技)。
柔印

2

我使用C ++ 14 std :: index_sequence(和函数返回类型作为模板参数RetT)从Johannes解决方案的变化:

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;
}

所有这些解决方案都可以解决最初的问题,但是老实说,从简单性和可维护性方面来说,这些模板材料不是朝着错误的方向发展吗?
xy 2015年

我认为使用C ++ 11和14可以使模板变得更好,更易理解。几年前,当我研究引擎盖下的模板的增强功能时,我感到非常沮丧。我同意,开发好的模板要比仅使用模板困难得多。
schwart 2015年

1
@xy首先,就模板复杂度而言,这没什么。其次,大多数帮助程序模板都是初始投资,可节省大量时间,以便稍后实例化时使用。最后,什么,您宁愿没有能力执行允许您执行的模板?您不能使用它,也不能留下似乎无关紧要的注释,这些注释似乎正在使其他程序员感到吃惊。
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.