Como armazenar argumentos de modelo variadic?

89

É possível armazenar um pacote de parâmetros de alguma forma para um uso posterior?

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

Então, mais tarde:

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

    //...

    add.act();
}
Eric B
fonte
3
Sim; armazene uma tupla e use algum tipo de pacote de índice para fazer a chamada.
Kerrek SB

Respostas:

67

Para realizar o que deseja fazer aqui, você terá que armazenar os argumentos do seu modelo em uma tupla:

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

Além disso, você terá que mudar um pouco o seu construtor. Em particular, inicializando argscom um std::make_tuplee também permitindo referências universais em sua lista de parâmetros:

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

Além disso, você teria que configurar um gerador de sequência muito parecido com este:

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 você pode implementar seu método em termos de um gerador:

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 é isso! Portanto, agora sua classe deve ser assim:

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

Aqui está o seu programa completo no Coliru.


Atualização: aqui está um método auxiliar pelo qual a especificação dos argumentos do modelo não é necessária:

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 novamente, aqui está outra demonstração.

0x499602D2
fonte
1
você poderia expandir um pouco sobre isso em sua resposta?
Eric B
1
Visto que Ts ... é um parâmetro do modelo de classe, não um parâmetro do modelo de função, Ts && ... não define um pacote de referências universais, pois não há dedução de tipo ocorrendo para o pacote de parâmetro. @jogojapan mostra a maneira correta de garantir que você possa passar referências universais ao construtor.
masrtis
3
Cuidado com as referências e a vida útil dos objetos! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());não é bom. Eu prefiro o comportamento de std::bind, que faz uma cópia de cada argumento, a menos que você desative-o com std::refou std::cref.
aschepler de
1
Acho que @jogojapan tem uma solução muito mais concisa e legível.
Tim Kuipers
1
@Riddick Mude args(std::make_tuple(std::forward<Args>(args)...))para args(std::forward<Args>(args)...). BTW, eu escrevi isso há muito tempo e não usaria esse código com o propósito de vincular uma função a alguns argumentos. Eu usaria apenas std::invoke()ou std::apply()hoje em dia.
0x499602D2
23

Você pode usar std::bind(f,args...)para isso. Ele irá gerar um objeto móvel e possivelmente copiável que armazena uma cópia do objeto de função e de cada um dos argumentos para uso posterior:

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

Observe que std::bindé uma função e você precisa armazenar, como membro de dados, o resultado de chamá-la. O tipo de dados desse resultado não é fácil de prever (o padrão nem mesmo especifica precisamente), então eu uso uma combinação de decltypee std::declvalpara calcular esse tipo de dados em tempo de compilação. Veja a definição Action::bind_typeacima.

Observe também como usei referências universais no construtor de modelo. Isso garante que você possa passar argumentos que não correspondem T...exatamente aos parâmetros do modelo de classe (por exemplo, você pode usar referências rvalue para alguns dos Te você os encaminhará como estão para a bindchamada.)

Nota final: Se você deseja armazenar argumentos como referências (de forma que a função que você passa possa modificá-los, ao invés de apenas usá-los), você precisa usá std::ref-los para envolvê-los em objetos de referência. Apenas passar um T &criará uma cópia do valor, não uma referência.

Código operacional no Coliru

jogojapan
fonte
Não é perigoso vincular rvalues? Não seriam invalidados quando addé definido em um escopo diferente de onde act()é chamado? O construtor não deveria obter em ConstrT&... argsvez de ConstrT&&... args?
Tim Kuipers
1
@Angelorf Desculpe pelo atraso na resposta. Você quer dizer rvalues ​​na chamada para bind()? Como bind()é garantido fazer cópias (ou mover para objetos recém-criados), não acho que haja um problema.
jogojapan
@jogojapan Nota rápida, MSVC17 requer que a função no construtor seja encaminhada para bind_ também (ou seja, bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Outshined
1
No inicializador, bind_(f, std::forward<ConstrT>(args)...)é um comportamento indefinido de acordo com o padrão, uma vez que esse construtor é definido pela implementação. bind_typeé especificado para ser copiado e / ou construtível por movimento, portanto bind_{std::bind(f, std::forward<ConstrT>(args)...)}, ainda deve funcionar.
joshtch
6

Esta pergunta era de C ++ 11 dias. Mas, para aqueles que o encontram nos resultados da pesquisa agora, algumas atualizações:

Um std::tuplemembro ainda é a maneira direta de armazenar argumentos em geral. (Uma std::bindsolução semelhante à de @ jogojapan também funcionará se você quiser apenas chamar uma função específica, mas não se quiser acessar os argumentos de outras maneiras, ou passar os argumentos para mais de uma função, etc.)

Em C ++ 14 e posterior, std::make_index_sequence<N>oustd::index_sequence_for<Pack...> pode substituir a helper::gen_seq<N>ferramenta vista na solução 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...>{});
    }
    // ...
};

No C ++ 17 e posterior, std::applypode ser usado para descompactar a tupla:

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

Aqui está um programa C ++ 17 completo mostrando a implementação simplificada. Eu também atualizei make_actionpara evitar tipos de referência no tuple, o que sempre foi ruim para argumentos rvalue e bastante arriscado para argumentos lvalue.

Aschepler
fonte
3

Acho que você tem um problema XY. Por que se dar ao trabalho de armazenar o pacote de parâmetros quando você poderia apenas usar um lambda no call center? ie,

#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
fonte
Porque em meu aplicativo, uma ação tem, na verdade, 3 functores diferentes, todos relacionados, e prefiro que as classes que a contenham contenham 1 ação e não 3 std :: function
Eric B