一致する関数ポインターを呼び出すためのタプルの「アンパック」


254

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>、すべてのタイプを1つずつ再帰的に評価するような別のテンプレートを作成しますが、関数呼び出しをディスパッチするための方法を確認できません。

これの本当の動機はやや複雑であり、それはとにかくほとんどが単なる学習課題です。別のインターフェースからの契約によりタプルを渡されたと想定できるので、変更することはできませんが、それを関数呼び出しにアンパックしたいという願望は私のものです。これstd::bindは、根本的な問題を回避する安価な方法として使用することを除外します。

を使用して呼び出しをディスパッチするきれいな方法std::tuple、または任意の将来のポイントまでいくつかの値と関数ポインタを格納/転送する同じ最終結果を達成する別のより良い方法は何ですか?


5
なぜあなたはただ使うことができないのですかauto saved = std::bind(f, a, b, c);...それから後で電話するだけsaved()ですか?
Charles Salvia

制御するための私のインターフェースとは限りません。他の人から契約でタプルを受け取り、その後それをやりたい。
フレキソ

回答:


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
うわー、私は開梱オペレーターがそのように使用できることを知りませんでした、これは素晴らしいです!
Luc Touraille、2011年

5
ヨハネス、あなたがこれを投稿してから2年以上経過していることを理解していますが、私が悩んでいることの1つは、struct gens一般的な定義( 同じ定義の拡張された派生から継承した定義)です。最終的には0でスペシャライゼーションに到達します。気分があなたに合い、予備のサイクルがあれば、それを拡張でき、それがどのように利用されるかは永遠に感謝します。そして、私はこれを100回賛成できればいいのに。このコードの接線で遊ぶのがもっと楽しくなりました。ありがとう。
WhozCraig 2013年

22
@WhozCraig:それが行うことは型を生成することです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 14

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

このスレッドの回答で一度述べるべきであると感じただけです(すでにコメントの1つに表示されている後)。


このスレッドには、基本的な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 2017

おかげで、私は2つの質問があります:1.なぜstd::make_unique直接渡せないのですか?具体的な関数インスタンスが必要ですか?2.なぜstd::move(ts)...我々は変更することができた場合[](auto... ts)[](auto&&... ts)
Xeverous

@Xeverous:1.シグネチャでは機能しません。std::make_uniqueタプルが必要ですstd::make_tuple。タプルは、アンパックされたタプルから、への別の呼び出しを介してのみ作成できます。これは私がラムダで行ったことです(ただし、を使用せずにタプルを一意のポインタにコピーするだけなので、非常に冗長ですcall)。
davidhigh 2017

1
これは今でなければなりません答え。
フューリーシュ

44

これは、誰かにとって役立つことを願って、ウッドランドの質問に対するヨハネスの解決策の完全にコンパイル可能なバージョンです。これは、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が必要なのですか?
2015年

@shoosh彼らは必要ないと思います。なぜそれらを追加したのか忘れてしまいました。ほぼ3年になります。しかし、私は、インスタンス化が機能することを示すためと思います。
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...>{}); }
};

これにはまだ1つのヘルパー関数(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...)ウォルターがclunkier構文を余儀なくされた理由です、std::index_sequence_for<Args...>{}
Quuxplusone 2014年

3
そして、どうやら2016
3

18

これは実現するのが少し複雑です(可能ですが)。これがすでに実装されているライブラリ、つまりBoost.Fusioninvoke関数)を使用することをお勧めします。おまけとして、Boost FusionはC ++ 03コンパイラでも動作します。


7

解決。最初に、いくつかのユーティリティのボイラープレート:

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

これらにより、一連のコンパイル時整数でラムダを呼び出すことができます。

void delayed_dispatch() {
  auto indexer = index_upto<sizeof...(Args)>();
  indexer([&](auto...Is){
    func(std::get<Is>(params)...);
  });
}

これで完了です。

index_uptoまたindex_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 );
}

コンパイラがアップグレードされ、ボブがあなたの叔父notstdであるstd場合に置き換えてください。


std::apply<-耳に
心地よい

@Flexoより少しだけ短く、index_upto柔軟性が低い。;)およびでfunc引数を逆に呼び出してみてください。確かに、一体誰がタプルから関数を逆方向に呼び出したいのでしょうか。index_uptostd::apply
Yakk-Adam Nevraumont 2017

マイナーポイント:std::tuple_size_vC ++ 17です。C++ 14ソリューションの場合、次のものに置き換える必要がありますtypename std::tuple_size<foo>::value
basteln '23

@basteln value型ではないことを願っています。しかしとにかく修正されました。
Yakk-Adam Nevraumont 2018

@ヤクいいえ、それはsizeof...(Types)です。なしのソリューションが好きtypenameです。
basteln 2018

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つの引数を取り、それをパラメーターパックに入れます。2番目のオーバーロードは、一致するパラメーターパックを受け取って実際の呼び出しを行います。最初のオーバーロードは、2番目が実行可能な唯一のケースで無効になります。


1
私はしばらく前に、これとひどく似たものに取り組んだ。時間があれば、もう一度確認して、現在の回答と比較してみましょう。
マイケルプライス

@MichaelPrice-純粋に学習の観点から、スタックポインターを破壊するひどいハックに煮詰められない(または、同様に規則固有のトリックを呼び出す)代替策を見つけることに興味があります。
フレキソ

2

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

これらのソリューションはすべて、最初の問題を解決する可能性がありますが、正直なところ、このテンプレートは、単純さと保守性の点で間違った方向に進んでいるのではないですか?
xy

C ++ 11と14を使用すると、テンプレートが大幅に改善され、理解しやすくなったと思います。数年前に、テンプレートを使ってブーストがどのように機能するかを調べたところ、本当に落胆しました。優れたテンプレートを開発することは、それらを使用することよりもはるかに難しいことに同意します。
シュヴァルト2015年

1
@xyまず、テンプレートの複雑さの観点から、これは何もありません。第2に、ほとんどのヘルパーテンプレートは、後でインスタンス化するときに節約された膨大な時間の初期投資です。最後に、あなたはむしろ何を考えていないテンプレートは、あなたが行うことができ何をする能力を持っていますか?あなたはそれを単に使用することができず、他のプログラマーを監視しているように見える無関係なコメントを残すことはできません。
underscore_d 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.