«Décompresser» un tuple pour appeler un pointeur de fonction correspondant

255

J'essaie de stocker dans un std::tuplenombre 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 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::tupledes 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 de moyen de le faire pour envoyer un appel de fonction.

La vraie motivation pour cela est un peu plus complexe et c'est surtout juste un exercice d'apprentissage de toute façon. Vous pouvez supposer que le tuple m'a été remis par contrat à partir d'une autre interface, donc ne peut pas être changé, mais que le désir de le décompresser dans un appel de fonction est le mien. Cela exclut l'utilisation std::bindcomme moyen bon marché de contourner le problème sous-jacent.

Quelle est une façon propre de répartir l'appel à l'aide de la std::tuple, ou une meilleure façon alternative d'obtenir le même résultat net de stockage / transfert de certaines valeurs et d'un pointeur de fonction jusqu'à un point futur arbitraire?

Flexo
la source
5
Pourquoi ne pouvez-vous pas simplement utiliser auto saved = std::bind(f, a, b, c);... puis appeler plus tard saved()?
Charles Salvia
Pas toujours mon interface à contrôler. Je reçois un tuple par contrat de quelqu'un d'autre et je souhaite en faire plus tard.
Flexo

Réponses:

62

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 pense que cela devrait être indiqué une fois dans une réponse dans ce fil (après qu'il soit déjà apparu dans l'un des commentaires).


La solution de base C ++ 14 est toujours manquante dans ce thread. 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 de code 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);    
}

DEMO

davidhigh
la source
Je ne peux pas faire fonctionner cette démo avec des pointeurs intelligents - qu'est-ce qui ne va pas ici? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous
@Xeverous: voulez-vous obtenir quelque chose comme ça ici ?
davidhigh
merci, j'ai 2 questions: 1. Pourquoi ne puis-je pas passer std::make_uniquedirectement? A-t-il besoin d'une instance de fonction concrète? 2. Pourquoi std::move(ts)...si nous pouvons changer [](auto... ts)pour [](auto&&... ts)?
Xeverous
@Xeverous: 1. ne fonctionne pas à partir des signatures: votre std::make_uniqueattend un tuple, et un tuple peut être créé à partir d'un tuple décompressé uniquement via un autre appel à std::make_tuple. C'est ce que j'ai fait dans le lambda (bien qu'il soit très redondant, car vous pouvez également simplement copier le tuple dans le pointeur unique sans aucune utilisation pour call).
davidhigh
1
Cela devrait maintenant être la réponse.
Fureeish
275

Vous devez créer un ensemble de paramètres de nombres 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) ...);
  }
// ...
Johannes Schaub - litb
la source
4
Wow, je ne savais pas que l'opérateur de déballage pouvait être utilisé comme ça, c'est bien!
Luc Touraille
5
Johannes, je me rends compte que cela fait plus de 2 ans que vous avez posté cela, mais la seule chose avec laquelle je me bats est la struct gensdéfinition générique (celle qui hérite d'une dérivation étendue de ladite même). Je vois qu'il finit par atteindre la spécialisation avec 0. Si l'ambiance vous convient et que vous avez les cycles de rechange, si vous pouvez développer cela et comment il est utilisé pour cela, je serais éternellement reconnaissant. Et j'aimerais pouvoir voter une centaine de fois. Je me suis plus amusé à jouer avec des tangentes de ce code. Merci.
WhozCraig
22
@WhozCraig: Ce qu'il fait, c'est générer un type seq<0, 1, .., N-1>. Comment ça marche: 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>. Le dernier type est spécialisé, la création seq<0, 1, 2, 3, 4>. Astuce assez intelligente.
mindvirus
2
@NirFriedman: Bien sûr, il suffit de remplacer la version non spécialisée de genspar:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78
11
Cela vaut la peine de faire écho à la réponse et aux commentaires de Walter à ce sujet: les gens n'ont plus besoin d'inventer leurs propres roues. La génération d'une séquence était si courante qu'elle était normalisée en C ++ 14 as std::integer_sequence<T, N>et sa spécialisation pour std::size_t, std::index_sequence<N>- plus leurs fonctions auxiliaires associées std::make_in(teger|dex)_sequence<>()et std::index_sequence_for<Ts...>(). Et en C ++ 17, il y a beaucoup d'autres bonnes choses intégrées dans la bibliothèque - en particulier y compris std::applyet std::make_from_tuple, qui gèreraient le déballage et l'appel des bits
underscore_d
44

Il s'agit d'une version compilable complète de la solution de Johannes à la question d'Awoodland, dans l'espoir qu'elle 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
Faheem Mitha
la source
Pourquoi avez-vous besoin des variables s et g?
shoosh
@shoosh Je suppose qu'ils ne sont pas nécessaires. J'oublie pourquoi j'ai ajouté ceux-ci; cela fait presque trois ans. Mais je suppose, pour montrer que l'instanciation fonctionne.
Faheem Mitha
42

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). Puisqu'il s'agit d'un idiome commun, la norme devrait peut-être le prendre en charge directement comme std::callpour 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 retardé 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); }
};
Walter
la source
8
A voté pour la mise en œuvre (proposée) de std::call. Le zoo chaotique integer_sequenceet index_sequenceles types d'assistance de C ++ 14 sont expliqués ici: en.cppreference.com/w/cpp/utility/integer_sequence Remarquez l'absence évidente de std::make_index_sequence(Args...), c'est pourquoi Walter a été forcé dans la syntaxe la plus maladroite std::index_sequence_for<Args...>{}.
Quuxplusone
3
Et apparemment voté en C ++ 17 depuis 3/2016 en tant que std :: apply (func, tup): en.cppreference.com/w/cpp/utility/apply
ddevienne
18

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.

Karel Petranek
la source
7

Solution. Tout d'abord, certains passe-partout utilitaires:

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

Ceux-ci vous permettent 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 terminé.

index_uptoet index_overvous permet de travailler avec des packs de paramètres sans avoir à générer de nouvelles surcharges externes.

Bien sûr, dans tu viens

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

Maintenant, si on aime ça, nous pouvons é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 obtenir le nettoyeur syntaxe prête à être expédiée.

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

il suffit de le remplacer notstdpar stdlorsque votre compilateur est mis à niveau et que bob est votre oncle.

Yakk - Adam Nevraumont
la source
std::apply<- de la musique à mes oreilles
Flexo
@Flexo Seulement un peu plus court index_uptoet moins flexible. ;) Essayez d'appeler funcavec les arguments en arrière avec index_uptoet std::applyrespectivement. Certes, qui diable veut invoquer une fonction d'un tuple vers l'arrière.
Yakk - Adam Nevraumont
Point mineur: std::tuple_size_vest C ++ 17, donc pour la solution C ++ 14 qui devrait être remplacée partypename std::tuple_size<foo>::value
basteln
@basteln J'espère que ce valuen'est pas un type. Mais fixe de toute façon.
Yakk - Adam Nevraumont
@Yakk Non, ça l'est sizeof...(Types). J'aime votre solution sans typename.
basteln
3

En réfléchissant un peu plus au problème en fonction 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 changer la mise en œuvre de delayed_dispatch():

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

Cela fonctionne en convertissant récursivement le std::tupleen un pack de paramètres à part entière. call_or_recurseest nécessaire comme spécialisation pour terminer la récursivité avec l'appel réel, qui décompresse simplement le pack de paramètres terminé.

Je ne suis pas sûr que ce soit de toute façon une "meilleure" solution, mais c'est une autre façon de penser et de la résoudre.


Comme autre solution alternative que vous pouvez utiliser enable_if, pour former quelque chose sans doute 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 de plus du tuple et le place dans un pack de paramètres. La deuxième surcharge prend un pack de paramètres correspondant, puis effectue l'appel réel, la première surcharge étant désactivée dans le seul et unique cas où le second serait viable.

Flexo
la source
1
J'ai travaillé sur quelque chose de terriblement similaire à cela il y a quelque temps. Si j'ai le temps, je vais jeter un deuxième coup d'œil et voir comment cela se compare aux réponses actuelles.
Michael Price du
@MichaelPrice - purement du point de vue de l'apprentissage, je serais intéressé à voir des solutions alternatives qui ne se résument pas à un horrible piratage qui bouscule le pointeur de la pile (ou appelle de la même façon des astuces spécifiques à la convention).
Flexo
2

Ma variation 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;
}
schwart
la source
Toutes ces solutions peuvent résoudre le problème initial, mais honnêtement les gars, ce truc de modèle ne va-t-il pas dans une mauvaise direction - en termes de simplicité et de maintenabilité ?
xy
Je pense que les modèles sont devenus beaucoup mieux et plus compréhensibles avec C ++ 11 et 14. Il y a quelques années, quand j'ai regardé ce que le boost faisait avec les modèles sous le capot, je me suis vraiment découragé. Je suis d'accord que développer de bons modèles est beaucoup plus difficile que de simplement les utiliser.
schwart
1
@xy Premièrement, en termes de complexité du modèle, ce n'est rien . Deuxièmement, la plupart des modèles d'aides sont un investissement initial pour une tonne de temps économisé lors de leur instanciation ultérieure. Enfin, quoi, préféreriez-vous ne pas avoir la possibilité de faire ce que les modèles vous permettent de faire? Vous ne pouvez tout simplement pas l'utiliser et ne pas laisser de commentaires non pertinents qui semblent contrôler d'autres programmeurs.
underscore_d