Come memorizzare gli argomenti del modello variadico?


89

È possibile memorizzare un pacchetto di parametri in qualche modo per un uso successivo?

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

Poi in seguito:

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

    //...

    add.act();
}

3
Sì; memorizzare una tupla e utilizzare una sorta di pacchetto indice per effettuare la chiamata.
Kerrek SB

Risposte:


67

Per ottenere ciò che vuoi fare qui, dovrai memorizzare gli argomenti del tuo modello in una tupla:

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

Inoltre, dovrai cambiare un po 'il tuo costruttore. In particolare, inizializzando argscon un std::make_tuplee consentendo anche riferimenti universali nell'elenco dei parametri:

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

Inoltre, dovresti impostare un generatore di sequenze molto simile a questo:

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

E puoi implementare il tuo metodo in termini di uno che prende un tale generatore:

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

E questo! Quindi ora la tua classe dovrebbe apparire così:

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

Ecco il tuo programma completo su Coliru.


Aggiornamento: ecco un metodo di supporto in base al quale non è necessaria la specifica degli argomenti del modello:

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

E ancora, ecco un'altra demo.


1
potresti approfondirlo un po 'nella tua risposta?
Eric B

1
Poiché Ts ... è un parametro del modello di classe, non un parametro del modello di funzione, Ts && ... non definisce un pacchetto di riferimenti universali poiché non si verifica alcuna deduzione di tipo per il pacchetto di parametri. @jogojapan mostra il modo corretto per assicurarsi di poter passare riferimenti universali al costruttore.
masrtis

3
Fai attenzione ai riferimenti e alla durata degli oggetti! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());non va bene. Preferisco il comportamento di std::bind, che crea una copia di ogni argomento a meno che non lo disabiliti con std::refo std::cref.
aschepler

1
Penso che @jogojapan abbia una soluzione molto più concisa e leggibile.
Tim Kuipers

1
@Riddick Cambia args(std::make_tuple(std::forward<Args>(args)...))in args(std::forward<Args>(args)...). BTW l'ho scritto molto tempo fa e non userei questo codice allo scopo di associare una funzione ad alcuni argomenti. Vorrei solo usare std::invoke()o al std::apply()giorno d'oggi.
0x499602D2

23

Puoi usare std::bind(f,args...)per questo. Genererà un oggetto mobile e possibilmente copiabile che memorizza una copia dell'oggetto funzione e di ciascuno degli argomenti per un uso successivo:

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

Si noti che std::bindè una funzione ed è necessario memorizzare, come membro dati, il risultato della chiamata. Il tipo di dati di quel risultato non è facile da prevedere (lo Standard non lo specifica nemmeno con precisione), quindi utilizzo una combinazione di decltypee std::declvalper calcolare quel tipo di dati in fase di compilazione. Vedere la definizione di cui Action::bind_typesopra.

Notare anche come ho usato i riferimenti universali nel costruttore basato su modelli. Ciò garantisce che tu possa passare argomenti che non corrispondono T...esattamente ai parametri del modello di classe (ad esempio puoi usare i riferimenti rvalue ad alcuni di Te li inoltrerai così com'è alla bindchiamata.)

Nota finale: se si desidera memorizzare gli argomenti come riferimenti (in modo che la funzione che si passa possa modificarli, anziché semplicemente utilizzarli), è necessario utilizzarli std::refper racchiuderli in oggetti di riferimento. Il semplice passaggio di T &creerà una copia del valore, non un riferimento.

Codice operativo su Coliru


Non è pericoloso legare i rvalues? Quelli non sarebbero invalidati quando addè definito in un ambito diverso da dove act()viene chiamato? Il costruttore non dovrebbe ottenere ConstrT&... argsinvece di ConstrT&&... args?
Tim Kuipers

1
@ Angelorf Scusa per la mia risposta in ritardo. Intendi rvalues ​​nella chiamata a bind()? Poiché bind()è garantito per fare copie (o per spostarsi in oggetti appena creati), non penso che ci possa essere un problema.
jogojapan

@jogojapan Nota rapida, MSVC17 richiede che anche la funzione nel costruttore venga inoltrata a bind_ (cioè bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Outshined l'

1
Nell'inizializzatore, bind_(f, std::forward<ConstrT>(args)...)è un comportamento non definito secondo lo standard, poiché quel costruttore è definito dall'implementazione. bind_typeè specificato per essere copia e / o trasportabile, tuttavia, bind_{std::bind(f, std::forward<ConstrT>(args)...)}dovrebbe comunque funzionare.
joshtch

4

Questa domanda era di C ++ 11 giorni. Ma per coloro che lo trovano nei risultati di ricerca ora, alcuni aggiornamenti:

Un std::tuplemembro è ancora il modo semplice per memorizzare gli argomenti in generale. (Una std::bindsoluzione simile a quella di @ jogojapan funzionerà anche se vuoi solo chiamare una funzione specifica, ma non se vuoi accedere agli argomenti in altri modi, o passare gli argomenti a più di una funzione, ecc.)

In C ++ 14 e versioni successive, std::make_index_sequence<N>ostd::index_sequence_for<Pack...> può sostituire lo helper::gen_seq<N>strumento visto nella soluzione 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...>{});
    }
    // ...
};

In C ++ 17 e versioni successive, std::applypuò essere utilizzato per occuparsi della decompressione della tupla:

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

Ecco un programma completo C ++ 17 che mostra l'implementazione semplificata. Ho anche aggiornato make_actionper evitare tipi di riferimento in tuple, che era sempre un male per gli argomenti rvalue e abbastanza rischioso per gli argomenti lvalue.


3

Penso che tu abbia un problema XY. Perché preoccuparsi di memorizzare il pacchetto di parametri quando si potrebbe semplicemente usare un lambda nel callsite? cioè

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

Perché nella mia applicazione, un'azione ha in realtà 3 diversi funtori che sono tutti correlati, e preferirei che le classi che la contengono contengano 1 azione e non 3 std :: function
Eric B
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.