web-dev-qa-db-fra.com

"décompresser" un tuple pour appeler un pointeur de fonction correspondante

J'essaie de stocker dans un std::Tuple un nombre variable de valeurs, qui seront ensuite utilisées comme arguments pour un appel à un pointeur de fonction qui correspond aux types stockés.

J'ai créé un exemple simplifié illustrant le problème que j'ai du mal à résoudre:

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

Normalement, pour les problèmes impliquant std::Tuple ou des modèles variadiques, j'écrirais un autre modèle, tel que template <typename Head, typename ...Tail>, afin d'évaluer récursivement tous les types un par un, mais je ne vois pas comment le faire pour distribuer une fonction. appel.

La motivation réelle est un peu plus complexe et il s’agit de toute façon d’un exercice d’apprentissage. Vous pouvez supposer que le tuple me est remis par contrat à partir d'une autre interface; vous ne pouvez donc pas le modifier, mais le désir de le décompresser en appel de fonction m'appartient. Ceci interdit d'utiliser std::bind comme moyen peu coûteux de contourner le problème sous-jacent.

Qu'est-ce qu'un moyen propre de répartir l'appel à l'aide de std::Tuple ou un autre moyen de parvenir au même résultat net consistant à stocker/transmettre certaines valeurs et un pointeur de fonction jusqu'à un point futur arbitraire?

246
Flexo

Vous devez créer un ensemble de paramètres et les décompresser.

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

Ceci est une version complète compilable de solution de Johanne à la question de awoodland, dans l’espoir que cela puisse être utile à quelqu'un. Cela a été testé avec un instantané de g ++ 4.7 sur Debian Squeeze.

###################
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

On peut utiliser le fichier SConstruct suivant

#####################
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"])

Sur ma machine, cela donne

g++-4.7 -o johannes.o -c -Wall -Werror -g -O3 -std=c++11 johannes.cc
g++-4.7 -o johannes johannes.o
45
Faheem Mitha

La solution C++ 17 consiste simplement à utiliser 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);

Je pensais juste que cela devrait être indiqué une fois dans une réponse dans ce fil (après que cela apparaisse déjà dans l'un des commentaires).


La solution de base C++ 14 est toujours manquante dans ce fil. EDIT: Non, c'est en fait là dans la réponse de Walter.

Cette fonction est donnée:

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

Appelez-le avec l'extrait suivant:

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

Exemple:

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

DÉMO

43
davidhigh

Voici une solution 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...>{}); }
};

Cela nécessite toujours une fonction d'assistance (call_func). Comme il s’agit d’un langage courant, la norme devrait peut-être la prendre en charge directement sous la forme std::call avec une implémentation possible

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

Ensuite, notre envoi différé devient

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

C'est un peu compliqué à réaliser (même si c'est possible). Je vous conseille d'utiliser une bibliothèque où cela est déjà implémenté, à savoir Boost.Fusion (la fonction invoke ). En prime, Boost Fusion fonctionne également avec les compilateurs C++ 03.

18
Karel Petranek

c ++ 14 solution. Tout d’abord, un peu de passe-partout pour les services publics:

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

Cela vous permet d’appeler un lambda avec une série d’entiers au moment de la compilation.

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

et nous avons fini.

index_upto et index_over vous permettent de travailler avec des ensembles de paramètres sans avoir à générer de nouvelles surcharges externes.

Bien sûr, dans c ++ 17 vous venez

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

Maintenant, si on aime ça, dans c ++ 14 on peut écrire:

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

relativement facilement et obtenez le nettoyeur c ++ 17 syntaxe prête à être expédiée.

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

il suffit de remplacer notstd par std lorsque votre compilateur est mis à niveau et que Bob est votre oncle.

6

En réfléchissant davantage au problème en fonction de la réponse donnée, j'ai trouvé un autre moyen de résoudre le même problème:

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

Ce qui nécessite de changer l'implémentation de delayed_dispatch() en:

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

Cela fonctionne en convertissant de manière récursive le std::Tuple en un paquet de paramètres à part entière. call_or_recurse est nécessaire en tant que spécialisation pour mettre fin à la récurrence avec l'appel réel, ce qui décompresse simplement le pack de paramètres terminé.

Je ne suis pas sûr qu'il s'agisse d'une "meilleure" solution, mais c'est une autre façon de penser et de la résoudre.


Une autre solution consiste à utiliser enable_if pour créer quelque chose de plus simple que ma solution précédente:

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

La première surcharge prend juste un argument supplémentaire du tuple et le place dans un pack de paramètres. La seconde surcharge prend un paquet de paramètres correspondant, puis effectue le véritable appel. La première surcharge est désactivée dans le seul et unique cas où la seconde serait viable.

3
Flexo

Ma variante de la solution de Johannes utilisant le std :: index_sequence C++ 14 (et le type de retour de fonction comme paramètre de modèle 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;
}
2
schwart