“desempaquetar” una tupla para llamar a un puntero de función coincidente

13 minutos de lectura

desempaquetar una tupla para llamar a un puntero de funcion
flexografía

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

Creé 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 con std::tuple o 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 forma de hacerlo para enviar una llamada de función.

La motivación real para esto es algo más compleja y, de todos modos, 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 el deseo de descomprimirlo en una llamada de función es mío. Esto descarta el uso std::bind como una forma barata de eludir el problema subyacente.

¿Cuál es una forma limpia de despachar la llamada usando el std::tupleo 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?

  • ¿Por qué no puedes simplemente usar auto saved = std::bind(f, a, b, c); … luego solo llama saved()?

    – Carlos Salvia

    11 de marzo de 2015 a las 17:11

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

    – Flexografía

    17 de enero de 2017 a las 17:58

Necesita crear un paquete de parámetros de números 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) ...);
  }
// ...

  • Wow, no sabía que el operador de desempaquetado podría usarse así, ¡esto es bueno!

    –Luc Touraille

    22 de octubre de 2011 a las 13:09

  • 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 el struct gens definición genérica (la que hereda de una expandido derivación de dicho 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, le estaría eternamente agradecido. Y desearía poder votar esto cien veces. Me he divertido más jugando con las tangentes de este código. Gracias.

    – WhozCraig

    18 de noviembre de 2013 a las 9:38


  • @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, creando seq<0, 1, 2, 3, 4>. Un truco bastante inteligente.

    – virus mental

    25 de abril de 2014 a las 15:38


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

    – marton78

    16 de abril de 2015 a las 14:11


  • Vale la pena repetir la respuesta de Walter y sus comentarios: la gente ya no necesita inventar sus propias ruedas. Generar una secuencia era tan común que se estandarizó en C++14 como std::integer_sequence<T, N> y la especialización de los mismos 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, que incluyen particularmente std::apply y std::make_from_tupleque manejaría los bits de desempaquetado y llamada

    – subrayado_d

    9 oct 2016 a las 14:22


desempaquetar una tupla para llamar a un puntero de funcion
davidalto

La solución C++17 es simplemente para 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 aún falta en este hilo. EDITAR: No, en realidad está allí 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>
}

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é ocurre aquí? http://coliru.stacked-crooked.com/a/8ea8bcc878efc3cb

    – Xeveroso

    7 sep 2017 a las 17:01

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

    – davidhigh

    7 sep 2017 a las 17:57


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

    – Xeveroso

    7 sep 2017 a las 18:01


  • @Xeverous: 1. no funciona a partir de las firmas: tu std::make_unique espera 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 hice en la lambda (aunque es muy redundante, ya que también puede simplemente copiar la tupla en el puntero único sin ningún uso para call).

    – davidhigh

    7 sep 2017 a las 18:09


  • Esto debería ser ahora los responder.

    – Furioso

    27 de junio de 2019 a las 22:27

desempaquetar una tupla para llamar a un puntero de funcion
Faheem Mitha

Esta es una versión compilable completa 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é necesita las variables s y g?

    – shoosh

    07/01/2015 a las 18:00

  • @shoosh Supongo que no son necesarios. Olvidé por qué los agregué; han pasado casi tres años. Pero supongo, para mostrar que la creación de instancias funciona.

    – Fahim Mitha

    7 de enero de 2015 a las 18:09

1647582549 112 desempaquetar una tupla para llamar a un puntero de funcion
walter

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, tal vez el estándar debería admitirlo directamente como std::call con 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 envío 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); }
};

1647582550 161 desempaquetar una tupla para llamar a un puntero de funcion
Karel Petranek

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 (los invocar función). Como beneficio adicional, Boost Fusion también funciona con compiladores C++03.

solución c ++ 14. Primero, algunos repetitivos 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_upto y index_over le permite trabajar con paquetes de parámetros sin tener que generar una nueva sobrecarga externa.

Por supuesto, en C++17 solo

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

Ahora, si nos gusta eso, en c++14 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))...
        );
      }
    );
  }
}

con relativa facilidad y obtenga la sintaxis más limpia de C++17 lista para su envío.

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

solo reemplaza notstd con std cuando tu compilador se actualiza y bob es tu tío.

Pensando un poco más en el problema según la respuesta dada, encontré 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)>
  }
};

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() para:

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

Esto funciona convirtiendo recursivamente el std::tuple en un paquete de parámetros por derecho propio. call_or_recurse se necesita como una especialización para terminar la recursividad con la llamada real, que simplemente desempaqueta el paquete de parámetros completo.

No estoy seguro de que esta sea de todos modos una solución “mejor”, pero es otra forma de pensar y resolverlo.


Como otra solución alternativa, puede utilizar enable_ifpara 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 lo 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 que la segunda sería viable.

  • 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 precio

    22/10/2011 a las 18:45

  • @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 llame de manera similar a los trucos específicos de la convención).

    – Flexografía

    22/10/2011 a las 19:45

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad