"Desempaquetar" una tupla para llamar a un puntero de función coincidente


255

Estoy tratando de almacenar un std::tuplenúmero variable de valores, que luego se utilizarán como argumentos para una llamada a un puntero de función que coincida con los tipos almacenados.

He creado un ejemplo simplificado que muestra el problema que estoy luchando por resolver:

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

Normalmente, para problemas relacionados std::tuplecon plantillas variadas, escribiría otra plantilla como template <typename Head, typename ...Tail>para evaluar recursivamente todos los tipos uno por uno, pero no puedo ver una manera de hacerlo para enviar una llamada a la función.

La verdadera motivación para esto es algo más compleja y, en general, es solo un ejercicio de aprendizaje. Puede suponer que me entregó la tupla por contrato desde otra interfaz, por lo que no se puede cambiar, pero que el deseo de descomprimirlo en una llamada de función es mío. Esto descarta usarlo std::bindcomo una forma barata de esquivar el problema subyacente.

¿Cuál es una manera limpia de despachar la llamada usando el std::tuple, o una mejor forma alternativa de lograr el mismo resultado neto de almacenar / reenviar algunos valores y un puntero de función hasta un punto futuro arbitrario?


55
¿Por qué no puedes usar auto saved = std::bind(f, a, b, c);... luego llamar saved()?
Charles Salvia

No siempre es mi interfaz para controlar. Recibo una tupla por contrato de otra persona y quiero hacer cosas con ella posteriormente.
Flexo

Respuestas:


275

Necesita construir un paquete de números de parámetros y descomprimirlos

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

44
Wow, no sabía que el operador de desempaque podría usarse así, ¡esto es bueno!
Luc Touraille

55
Johannes, me doy cuenta de que han pasado más de 2 años desde que publicaste esto, pero lo único con lo que estoy luchando es la struct gensdefinición genérica (la que hereda de una derivación expandida de lo mismo). Veo que finalmente llega a la especialización con 0. Si el estado de ánimo le conviene y tiene los ciclos de repuesto, si puede ampliar eso y cómo se utiliza para esto, estaría eternamente agradecido. Y desearía poder votar esto cientos de veces. Me he divertido más jugando con las tangentes de este código. Gracias.
WhozCraig

22
@WhozCraig: Lo que hace es generar un tipo seq<0, 1, .., N-1>. Cómo funciona: 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>. El último tipo es especializado, crear seq<0, 1, 2, 3, 4>. Truco bastante inteligente.
mindvirus

2
@NirFriedman: Claro, solo reemplace la versión no especializada de gens:template <int N, int... S> struct gens { typedef typename gens<N-1, N-1, S...>::type type; };
marton78

11
Vale la pena hacerse eco de la respuesta y los comentarios de Walter al respecto: la gente ya no necesita inventar sus propias ruedas. La generación de una secuencia era tan común que se estandarizó en C ++ 14 std::integer_sequence<T, N>y su especialización para std::size_t, std::index_sequence<N>más sus funciones auxiliares asociadas std::make_in(teger|dex)_sequence<>()y std::index_sequence_for<Ts...>(). Y en C ++ 17 hay muchas otras cosas buenas integradas en la biblioteca, particularmente incluyendo std::applyy std::make_from_tuple, que manejaría el desempaquetado y los bits de llamada
underscore_d

62

La solución C ++ 17 es simplemente usar 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);

Simplemente sentí que debería indicarse una vez en una respuesta en este hilo (después de que ya apareció en uno de los comentarios).


La solución básica de C ++ 14 todavía falta en este hilo. EDITAR: No, en realidad está ahí en la respuesta de Walter.

Esta función está dada:

void f(int a, double b, void* c)
{
      std::cout << a << ":" << b << ":" << c << std::endl;
}

Llámalo con el siguiente fragmento:

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

Ejemplo:

int main()
{
    std::tuple<int, double, int*> t;
    //or std::array<int, 3> t;
    //or std::pair<int, double> t;
    call(f, t);    
}

MANIFESTACIÓN


No puedo hacer que esta demostración funcione con punteros inteligentes, ¿qué hay de malo aquí? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb
Xeverous

@ Xeverous: ¿quieres obtener algo como esto aquí ?
davidhigh

gracias, tengo 2 preguntas: 1. ¿Por qué no puedo pasar std::make_uniquedirectamente? ¿Necesita instancia de función concreta? 2. ¿Por qué std::move(ts)...si podemos cambiar [](auto... ts)a [](auto&&... ts)?
Xeverous

@Xeverous: 1. no funciona a partir de las firmas: std::make_uniqueespera una tupla, y una tupla se puede crear a partir de una tupla desempaquetada solo a través de otra llamada a std::make_tuple. Esto es lo que he hecho en el lambda (aunque es muy redundante, ya que también puede simplemente copiar la tupla en el puntero único sin ningún uso call).
davidhigh

1
Esta debería ser ahora la respuesta.
Fureeish

44

Esta es una versión completa compilable de la solución de Johannes a la pregunta de awoodland, con la esperanza de que pueda ser útil para alguien. Esto se probó con una instantánea de g ++ 4.7 en 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

Uno puede usar el siguiente archivo SConstruct

#####################
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"])

En mi máquina, esto da

g++-4.7 -o johannes.o -c -Wall -Werror -g -O3 -std=c++11 johannes.cc
g++-4.7 -o johannes johannes.o

¿Por qué necesitas las variables syg?
shoosh

@shoosh Supongo que no son necesarios. Olvidé por qué agregué esos; Han pasado casi tres años. Pero supongo que para mostrar que la instanciación funciona.
Faheem Mitha

42

Aquí hay una solución 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...>{}); }
};

Esto todavía necesita una función auxiliar ( call_func). Dado que este es un idioma común, quizás el estándar debería admitirlo directamente como std::callcon una posible implementación

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

Entonces nuestro despacho retrasado se convierte en

template <typename ...Args>
struct save_it_for_later
{
  std::tuple<Args...> params;
  std::function<void(Args...)> func;
  void delayed_dispatch()
  { std::call(func,params); }
};

8
Votaron por la implementación (propuesta) de std::call. El zoológico caótico de C ++ 14 integer_sequencey los index_sequencetipos de ayuda se explican aquí: en.cppreference.com/w/cpp/utility/integer_sequence Observe la notable ausencia de std::make_index_sequence(Args...), por lo que Walter se vio obligado a entrar en la sintaxis más complicada std::index_sequence_for<Args...>{}.
Quuxplusone

3
Y aparentemente votó en C ++ 17 desde 3/2016 como std :: apply (func, tup): en.cppreference.com/w/cpp/utility/apply
ddevienne

18

Esto es un poco complicado de lograr (aunque es posible). Le aconsejo que use una biblioteca donde esto ya está implementado, a saber, Boost.Fusion (la función de invocación ). Como beneficio adicional, Boost Fusion también funciona con compiladores C ++ 03.


7

solución. Primero, algunos ejemplos de utilidad:

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

Estos le permiten llamar a una lambda con una serie de enteros en tiempo de compilación.

void delayed_dispatch() {
  auto indexer = index_upto<sizeof...(Args)>();
  indexer([&](auto...Is){
    func(std::get<Is>(params)...);
  });
}

y hemos terminado

index_uptoy le index_overpermite trabajar con paquetes de parámetros sin tener que generar nuevas sobrecargas externas.

Por supuesto, en tu solo

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

Ahora, si nos gusta eso, en podemos escribir:

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

relativamente fácil y obtener el limpiador sintaxis lista para enviar.

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

simplemente reemplace notstdcon stdcuando su compilador se actualice y bob sea su tío.


std::apply<- música para mis oídos
Flexo

@Flexo Solo un poco más corto index_uptoy menos flexible. ;) Intente llamar funccon los argumentos al revés con index_uptoy std::applyrespectivamente. Es cierto que quién diablos quiere invocar una función de una tupla al revés.
Yakk - Adam Nevraumont

Punto menor: std::tuple_size_ves C ++ 17, por lo que para la solución C ++ 14 que tendría que ser reemplazado portypename std::tuple_size<foo>::value
basteln

@basteln Espero valueque no sea un tipo. Pero arreglado de todos modos.
Yakk - Adam Nevraumont

@Yakk No, lo es sizeof...(Types). Me gusta tu solución sin el typename.
basteln

3

Pensando en el problema un poco más en función de la respuesta dada, he encontrado otra forma de resolver el mismo problema:

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

Lo que requiere cambiar la implementación de delayed_dispatch()a:

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

Esto funciona mediante la conversión recursiva std::tupleen un paquete de parámetros por derecho propio. call_or_recursees necesario como especialización para terminar la recursión con la llamada real, que simplemente desempaqueta el paquete de parámetros completado.

No estoy seguro de que sea una solución "mejor", pero es otra forma de pensar y resolverlo.


Como otra solución alternativa que puede usar enable_if, para formar algo posiblemente más simple que mi solución anterior:

#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 primera sobrecarga solo toma un argumento más de la tupla y la coloca en un paquete de parámetros. La segunda sobrecarga toma un paquete de parámetros coincidentes y luego realiza la llamada real, con la primera sobrecarga deshabilitada en el único caso en el que la segunda sería viable.


1
Trabajé en algo terriblemente similar a esto hace un tiempo. Si tengo tiempo, echaré un segundo vistazo y veré cómo se compara con las respuestas actuales.
Michael Price

@MichaelPrice: puramente desde la perspectiva del aprendizaje, me interesaría ver cualquier solución alternativa que no se reduzca a un truco horrible que estropee el puntero de la pila (o que llame a trucos específicos de la convención).
Flexo

2

Mi variación de la solución de Johannes usando C ++ 14 std :: index_sequence (y el tipo de retorno de función como parámetro de plantilla 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;
}

Todas esas soluciones pueden resolver el problema inicial, pero, sinceramente, ¿no está yendo esta plantilla en una dirección incorrecta, en términos de simplicidad y facilidad de mantenimiento ?
xy

Creo que las plantillas se volvieron mucho mejores y más comprensibles con C ++ 11 y 14. Hace unos años, cuando vi lo que hace el impulso con las plantillas bajo el capó, me desanimé mucho. Estoy de acuerdo en que desarrollar buenas plantillas es significativamente más difícil que solo usarlas.
schwart

1
@xy En primer lugar, en términos de complejidad de la plantilla, esto no es nada . En segundo lugar, la mayoría de las plantillas de ayuda son una inversión inicial por una tonelada de tiempo ahorrada al instanciarlas más tarde. Por último, ¿qué preferirías no tener la capacidad de hacer qué plantillas te permiten hacer? Simplemente no podría usarlo y no dejar comentarios irrelevantes que parecen estar controlando a otros programadores.
underscore_d
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.