289 votes

"déballer" un tuple pour appeler un pointeur de fonction correspondant

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

J'ai créé un exemple simplifié montrant le problème que je m'efforce de 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 comme template <typename Head, typename ...Tail> pour évaluer récursivement tous les types un par un, mais je ne vois pas comment le faire pour répartir un appel de fonction.

La motivation réelle de cette démarche est un peu plus complexe et il s'agit surtout d'un exercice d'apprentissage. Vous pouvez supposer que le tuple m'est fourni par contrat par une autre interface, et qu'il ne peut donc pas être modifié, mais que le désir de le décomposer en un appel de fonction est le mien. Cela exclut l'utilisation de std::bind comme un moyen bon marché de contourner le problème sous-jacent.

Quel est le moyen le plus simple de lancer l'appel en utilisant la fonction std::tuple ou un meilleur moyen d'obtenir le même résultat net en stockant/transférant certaines valeurs et un pointeur de fonction jusqu'à un point futur arbitraire ?

5 votes

Pourquoi ne pouvez-vous pas simplement utiliser auto saved = std::bind(f, a, b, c); ... puis plus tard, appelez simplement saved() ?

0 votes

Ce n'est pas toujours mon interface à contrôler. Je reçois un tuple par contrat de quelqu'un d'autre et je veux en faire quelque chose par la suite.

291voto

Johannes Schaub - litb Points 256113

Vous devez construire un paquet de paramètres de chiffres et les déballer.

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 votes

Wow, je ne savais pas que l'opérateur de déballage pouvait être utilisé comme ça, c'est sympa !

6 votes

Johannes, je me rends compte que cela fait plus de deux ans que tu as posté ce message, mais la seule chose qui me pose problème est le struct gens définition générique (celle qui hérite d'une élargi dérivé de ce dernier). Je vois qu'il atteint finalement la spécialisation avec 0. Si l'humeur vous convient et que vous avez les cycles libres, si vous pouvez développer cela, et comment il est utilisé pour cela, je vous en serais éternellement reconnaissant. Et j'aimerais pouvoir voter cent fois plus. J'ai eu plus de plaisir à jouer avec les tangentes de ce code. Merci.

0 votes

Le site type n'apparaîtra pas tant que la récursion ne sera pas terminée, lorsque le paramètre comptera jusqu'à 0 comme vous le dites. C'est à ce moment que tous les nombres de la plage [0, N-1] sont entrés dans la spécification du modèle, qui est le paquet de paramètres utilisé pour créer la séquence.

46voto

Faheem Mitha Points 1589

Il s'agit d'une version compilable complète de La solution de Johannes à la question de awoodland, dans l'espoir que cela puisse être utile à quelqu'un. Ceci 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

0 votes

Pourquoi avez-vous besoin des variables s et g ?

0 votes

@shoosh Je suppose qu'ils ne sont pas nécessaires. J'ai oublié pourquoi je les ai ajoutés ; cela fait presque trois ans. Mais je suppose que c'était pour montrer que l'instanciation fonctionne.

43voto

Walter Points 7554

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 encore une fonction d'aide ( call_func ). Comme il s'agit d'un idiome courant, la norme devrait peut-être le prendre en charge directement en tant que std::call avec possibilité de mise en œuvre

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

Alors notre répartition retardée 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); }
};

8 votes

Upvoted pour la mise en œuvre (proposée) de std::call . Le zoo chaotique de C++14 de integer_sequence y index_sequence Les types d'aide sont expliqués ici : fr.cppreference.com/w/cpp/utility/integer_sequence Remarquez l'absence manifeste de std::make_index_sequence(Args...) c'est pourquoi Walter a été contraint d'utiliser la syntaxe plus complexe std::index_sequence_for<Args...>{} .

3 votes

Et apparemment voté dans C++17 depuis 3/2016 comme std::apply(func, tup) : fr.cppreference.com/w/cpp/utility/apply

18voto

Karel Petranek Points 10740

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 (le invoquez fonction). En prime, Boost Fusion fonctionne également avec les compilateurs C++03.

3voto

Flexo Points 39273

En réfléchissant un peu plus au problème sur la base de la réponse donnée, j'ai trouvé une autre façon 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 modifier l'implémentation de delayed_dispatch() à :

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

Cela fonctionne en convertissant de manière récursive le fichier std::tuple en un pack de paramètres à part entière. call_or_recurse est nécessaire en tant que spécialisation pour terminer la récursion avec l'appel réel, qui ne fait que dépaqueter le paquet de paramètres terminé.

Je ne suis pas sûr que ce soit une "meilleure" solution, mais c'est une autre façon d'envisager et de résoudre le problème.


Comme autre solution alternative, vous pouvez utiliser enable_if pour former quelque chose d'un peu 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 simplement un argument supplémentaire du tuple et le place dans un paquet de paramètres. La seconde surcharge prend un paquet de paramètres correspondant et effectue ensuite l'appel réel, la première surcharge étant désactivée dans le seul et unique cas où la seconde serait viable.

1 votes

J'ai travaillé sur quelque chose de très similaire à ça il y a quelques temps. Si j'ai le temps, j'y jetterai un coup d'œil et verrai comment il se compare aux réponses actuelles.

0 votes

@MichaelPrice - d'un point de vue purement didactique, je serais intéressé de voir toute solution alternative qui ne se résume pas à un hack horrible bâclant le pointeur de pile (ou des astuces spécifiques aux conventions d'appel similaires).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X