¿Cómo almacenar argumentos de plantilla variadic?

89

¿Es posible almacenar un paquete de parámetros de alguna manera para un 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
    }
}

Luego, más tarde:

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

    //...

    add.act();
}
Eric B
fuente
3
Si; almacenar una tupla y utilizar algún tipo de paquete de índice para realizar la llamada.
Kerrek SB
¿Responde esto a tu pregunta? C ++ Cómo almacenar un paquete de parámetros como una variable
user202729

Respuestas:

67

Para lograr lo que quiere que se haga aquí, tendrá que almacenar los argumentos de su plantilla en una tupla:

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

Además, tendrás que cambiar un poco tu constructor. En particular, inicializar argscon un std::make_tupley también permitir referencias universales en su lista de parámetros:

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

Además, tendrías que configurar un generador de secuencias como 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...> {};
}

Y puede implementar su método en términos de uno que tome tal generador:

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

¡Y eso es todo! Así que ahora tu clase debería verse así:

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

Aquí está su programa completo sobre Coliru.


Actualización: aquí hay un método auxiliar por el cual la especificación de los argumentos de la plantilla no es necesaria:

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

Y nuevamente, aquí hay otra demostración.

0x499602D2
fuente
1
¿Podría ampliarlo un poco en su respuesta?
Eric B
1
Dado que Ts ... es un parámetro de plantilla de clase, no un parámetro de plantilla de función, Ts && ... no define un paquete de referencias universales ya que no se produce ninguna deducción de tipo para el paquete de parámetros. @jogojapan muestra la forma correcta de asegurarse de que puede pasar referencias universales al constructor.
masrtis
3
¡Cuidado con las referencias y la vida útil de los objetos! void print(const std::string&); std::string hello(); auto act = make_action(print, hello());no es bueno. Preferiría el comportamiento de std::bind, que hace una copia de cada argumento a menos que lo desactive con std::refo std::cref.
aschepler
1
Creo que @jogojapan tiene una solución mucho más concisa y legible.
Tim Kuipers
1
@Riddick Cambiar args(std::make_tuple(std::forward<Args>(args)...))a args(std::forward<Args>(args)...). Por cierto, escribí esto hace mucho tiempo y no usaría este código con el propósito de vincular una función a algunos argumentos. Solo usaría std::invoke()o std::apply()hoy en día.
0x499602D2
23

Puede utilizar std::bind(f,args...)para esto. Generará un objeto movible y posiblemente copiable que almacena una copia del objeto de función y de cada uno de los argumentos para su 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;
}

Note que std::bindes una función y necesita almacenar, como miembro de datos, el resultado de llamarla. El tipo de datos de ese resultado no es fácil de predecir (el Estándar ni siquiera lo especifica con precisión), así que utilizo una combinación de decltypey std::declvalpara calcular ese tipo de datos en el momento de la compilación. Vea la definición de Action::bind_typearriba.

También observe cómo usé referencias universales en el constructor con plantilla. Esto asegura que pueda pasar argumentos que no coincidan T...exactamente con los parámetros de la plantilla de clase (por ejemplo, puede usar referencias rvalue a algunos de los Ty los enviará como están a la bindllamada).

Nota final: si desea almacenar argumentos como referencias (para que la función que pase pueda modificarlos, en lugar de simplemente usarlos), debe usarlos std::refpara envolverlos en objetos de referencia. Simplemente pasar un T &creará una copia del valor, no una referencia.

Código operativo en Coliru

jogojapan
fuente
¿No es peligroso vincular rvalues? ¿No se invalidarían aquellos cuando addse definen en un ámbito diferente al de dónde act()se llama? ¿No debería el constructor obtener en ConstrT&... argslugar de ConstrT&&... args?
Tim Kuipers
1
@Angelorf Perdón por mi respuesta tardía. ¿Te refieres a rvalues ​​en la llamada a bind()? Dado que bind()está garantizado para hacer copias (o moverse a objetos recién creados), no creo que pueda haber un problema.
jogojapan
@jogojapan Nota rápida, MSVC17 requiere que la función en el constructor se reenvíe a bind_ también (es decir, bind_ (std :: forward <std :: function <void (T ...) >> (f), std :: forward < ConstrT> (args) ...))
Eclipsado el
1
En el inicializador, bind_(f, std::forward<ConstrT>(args)...)hay un comportamiento indefinido según el estándar, ya que ese constructor está definido por la implementación. bind_typeestá especificado para ser copia y / o mover-construible, por lo bind_{std::bind(f, std::forward<ConstrT>(args)...)}que aún debería funcionar.
joshtch
4

Esta pregunta fue de C ++ 11 días. Pero para aquellos que lo encuentran en los resultados de búsqueda ahora, algunas actualizaciones:

Un std::tuplemiembro sigue siendo la forma más sencilla de almacenar argumentos en general. (Una std::bindsolución similar a la de @ jogojapan también funcionará si solo desea llamar a una función específica, pero no si desea acceder a los argumentos de otras formas, o pasar los argumentos a más de una función, etc.)

En C ++ 14 y posterior, std::make_index_sequence<N>ostd::index_sequence_for<Pack...> puede reemplazar la helper::gen_seq<N>herramienta que se ve en la solución 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...>{});
    }
    // ...
};

En C ++ 17 y posteriores, std::applyse puede utilizar para encargarse de desempaquetar la tupla:

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

Aquí hay un programa completo de C ++ 17 que muestra la implementación simplificada. También actualicé make_actionpara evitar tipos de referencia en tuple, lo que siempre era malo para los argumentos rvalue y bastante arriesgado para los argumentos lvalue.

aschepler
fuente
3

Creo que tienes un problema XY. ¿Por qué tomarse la molestia de almacenar el paquete de parámetros cuando podría usar una lambda en el sitio de llamadas? es decir,

#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
fuente
Debido a que en mi solicitud, una acción en realidad tiene 3 palabras funcionales diferentes que están todos relacionados, y yo prefiero que lo contienen clases contiene 1 Acción, y no 3 std :: función
Eric B