Comment stocker les arguments de modèles variadiques?

89

Est-il possible de stocker un pack de paramètres d'une manière ou d'une autre pour une utilisation ultérieure?

template <typename... T>
class Action {
private:        
    std::function<void(T...)> f;
    T... args;  // <--- something like this
public:
    Action(std::function<void(T...)> f, T... args) : f(f), args(args) {}
    void act(){
        f(args);  // <--- such that this will be possible
    }
}

Puis plus tard:

void main(){
    Action<int,int> add([](int x, int y){std::cout << (x+y);}, 3, 4);

    //...

    add.act();
}
Eric B
la source
3
Oui; stocker un tuple et utiliser une sorte de pack d'index pour effectuer l'appel.
Kerrek SB
Est-ce que cela répond à votre question? C ++ Comment stocker un pack de paramètres en tant que variable
user202729

Réponses:

67

Pour accomplir ce que vous voulez faire ici, vous devrez stocker vos arguments de modèle dans un tuple:

std::tuple<Ts...> args;

De plus, vous devrez changer un peu de constructeur. En particulier, l'initialisation argsavec un std::make_tupleet autorisant également les références universelles dans votre liste de paramètres:

template <typename F, typename... Args>
Action(F&& func, Args&&... args)
    : f(std::forward<F>(func)),
      args(std::forward<Args>(args)...)
{}

De plus, vous devrez configurer un générateur de séquence semblable à celui-ci:

namespace helper
{
    template <int... Is>
    struct index {};

    template <int N, int... Is>
    struct gen_seq : gen_seq<N - 1, N - 1, Is...> {};

    template <int... Is>
    struct gen_seq<0, Is...> : index<Is...> {};
}

Et vous pouvez implémenter votre méthode en prenant un tel générateur:

template <typename... Args, int... Is>
void func(std::tuple<Args...>& tup, helper::index<Is...>)
{
    f(std::get<Is>(tup)...);
}

template <typename... Args>
void func(std::tuple<Args...>& tup)
{
    func(tup, helper::gen_seq<sizeof...(Args)>{});
}

void act()
{
   func(args);
}

Et c'est ça! Alors maintenant, votre classe devrait ressembler à ceci:

template <typename... Ts>
class Action
{
private:
    std::function<void (Ts...)> f;
    std::tuple<Ts...> args;
public:
    template <typename F, typename... Args>
    Action(F&& func, Args&&... args)
        : f(std::forward<F>(func)),
          args(std::forward<Args>(args)...)
    {}

    template <typename... Args, int... Is>
    void func(std::tuple<Args...>& tup, helper::index<Is...>)
    {
        f(std::get<Is>(tup)...);
    }

    template <typename... Args>
    void func(std::tuple<Args...>& tup)
    {
        func(tup, helper::gen_seq<sizeof...(Args)>{});
    }

    void act()
    {
        func(args);
    }
};

Voici votre programme complet sur Coliru.


Mise à jour: Voici une méthode d'assistance par laquelle la spécification des arguments du modèle n'est pas nécessaire:

template <typename F, typename... Args>
Action<Args...> make_action(F&& f, Args&&... args)
{
    return Action<Args...>(std::forward<F>(f), std::forward<Args>(args)...);
}

int main()
{
    auto add = make_action([] (int a, int b) { std::cout << a + b; }, 2, 3);

    add.act();
}

Et encore une fois, voici une autre démo.

0x499602D2
la source
1
pourriez-vous nous en dire un peu plus dans votre réponse?
Eric B
1
Puisque Ts ... est un paramètre de modèle de classe, pas un paramètre de modèle de fonction, Ts && ... ne définit pas un pack de références universelles car il n'y a pas de déduction de type pour le pack de paramètres. @jogojapan montre la manière correcte de garantir que vous pouvez passer des références universelles au constructeur.
masrtis
3
Attention aux références et à la durée de vie des objets! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());n'est pas bon. Je préfère le comportement de std::bind, qui fait une copie de chaque argument à moins que vous ne le désactiviez avec std::refou std::cref.
aschepler
1
Je pense que @jogojapan a une solution beaucoup plus concise et lisible.
Tim Kuipers
1
@Riddick changement args(std::make_tuple(std::forward<Args>(args)...))à args(std::forward<Args>(args)...). BTW je l'ai écrit il y a longtemps et je n'utiliserais pas ce code dans le but de lier une fonction à certains arguments. Je voudrais juste utiliser std::invoke()ou de std::apply()nos jours.
0x499602D2
23

Vous pouvez utiliser std::bind(f,args...)pour cela. Il générera un objet mobile et éventuellement copiable qui stockera une copie de l'objet fonction et de chacun des arguments pour une utilisation ultérieure:

#include <iostream>
#include <utility>
#include <functional>

template <typename... T>
class Action {
public:

  using bind_type = decltype(std::bind(std::declval<std::function<void(T...)>>(),std::declval<T>()...));

  template <typename... ConstrT>
  Action(std::function<void(T...)> f, ConstrT&&... args)
    : bind_(f,std::forward<ConstrT>(args)...)
  { }

  void act()
  { bind_(); }

private:
  bind_type bind_;
};

int main()
{
  Action<int,int> add([](int x, int y)
                      { std::cout << (x+y) << std::endl; },
                      3, 4);

  add.act();
  return 0;
}

Notez qu'il std::binds'agit d'une fonction et que vous devez stocker, en tant que membre de données, le résultat de son appel. Le type de données de ce résultat n'est pas facile à prédire (le Standard ne le spécifie même pas avec précision), j'utilise donc une combinaison de decltypeet std::declvalpour calculer ce type de données au moment de la compilation. Voir la définition de Action::bind_typeci - dessus.

Notez également comment j'ai utilisé des références universelles dans le constructeur basé sur un modèle. Cela garantit que vous pouvez passer des arguments qui ne correspondent pas T...exactement aux paramètres du modèle de classe (par exemple, vous pouvez utiliser des références rvalue à certains des Tet vous les transmettrez tels quels à l' bindappel.)

Remarque finale: si vous souhaitez stocker des arguments en tant que références (afin que la fonction que vous transmettez puisse les modifier, plutôt que simplement les utiliser), vous devez les utiliser std::refpour les envelopper dans des objets de référence. Le simple fait de passer un T &créera une copie de la valeur, pas une référence.

Code opérationnel sur Coliru

jogojapan
la source
N'est-il pas dangereux de lier des valeurs? Est-ce que ceux-ci ne seraient pas invalidés lorsqu'ils addsont définis dans une portée différente de celle où act()est appelée? Le constructeur ne devrait-il pas obtenir ConstrT&... argsplutôt que ConstrT&&... args?
Tim Kuipers
1
@Angelorf Désolé pour ma réponse tardive. Vous voulez dire des valeurs dans l'appel à bind()? Comme il bind()est garanti de faire des copies (ou de se déplacer dans des objets fraîchement créés), je ne pense pas qu'il puisse y avoir de problème.
jogojapan
@jogojapan Note rapide, MSVC17 nécessite que la fonction du constructeur soit également transmise à bind_ (c'est-à-dire bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Surclassé
1
Dans l'initialiseur, le bind_(f, std::forward<ConstrT>(args)...)comportement n'est pas défini selon la norme, car ce constructeur est défini par l'implémentation. bind_typeest spécifié comme étant une copie et / ou un déplacement constructible, cependant, bind_{std::bind(f, std::forward<ConstrT>(args)...)}devrait toujours fonctionner.
joshtch
4

Cette question était de C ++ 11 jours. Mais pour ceux qui le trouvent dans les résultats de recherche maintenant, quelques mises à jour:

Un std::tuplemembre est toujours le moyen le plus simple de stocker des arguments en général. (Une std::bindsolution similaire à celle de @ jogojapan fonctionnera également si vous souhaitez simplement appeler une fonction spécifique, mais pas si vous souhaitez accéder aux arguments d'une autre manière, ou passer les arguments à plus d'une fonction, etc.)

Dans C ++ 14 et versions ultérieures, std::make_index_sequence<N>oustd::index_sequence_for<Pack...> peut remplacer l' helper::gen_seq<N>outil vu dans la solution de 0x499602D2 :

#include <utility>

template <typename... Ts>
class Action
{
    // ...
    template <typename... Args, std::size_t... Is>
    void func(std::tuple<Args...>& tup, std::index_sequence<Is...>)
    {
        f(std::get<Is>(tup)...);
    }

    template <typename... Args>
    void func(std::tuple<Args...>& tup)
    {
        func(tup, std::index_sequence_for<Args...>{});
    }
    // ...
};

Dans C ++ 17 et versions ultérieures, std::applypeut être utilisé pour prendre soin de décompresser le tuple:

template <typename... Ts>
class Action
{
    // ...
    void act() {
        std::apply(f, args);
    }
};

Voici un programme C ++ 17 complet montrant l'implémentation simplifiée. J'ai également mis make_actionà jour pour éviter les types de référence dans le tuple, ce qui était toujours mauvais pour les arguments rvalue et assez risqué pour les arguments lvalue.

aschepler
la source
3

Je pense que vous avez un problème XY. Pourquoi se donner la peine de stocker le pack de paramètres alors que vous pourriez simplement utiliser un lambda sur le site d'appel? c'est à dire,

#include <functional>
#include <iostream>

typedef std::function<void()> Action;

void callback(int n, const char* s) {
    std::cout << s << ": " << n << '\n';
}

int main() {
    Action a{[]{callback(13, "foo");}};
    a();
}
Casey
la source
Parce que dans mon application, une action a en fait 3 foncteurs différents qui sont tous liés, et je préfère les classes qui la contiennent contiennent 1 action, et non 3 std :: function
Eric B