utilice - tuplas en c++




"Desempaquetar" una tupla para llamar a un puntero de función correspondiente (6)

Estoy intentando 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.

He creado un ejemplo simplificado que muestra el problema que estoy tratando de 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 las plantillas std::tuple o variadic, 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 que para despachar una llamada de función.

La motivación real para esto es algo más compleja y, de todas formas, es solo un ejercicio de aprendizaje. Puede suponer que me entregué la tupla por contrato desde otra interfaz, por lo que no se puede cambiar, pero el deseo de desempaquetarlo en una llamada de función es mío. Esto descarta el uso de std::bind como una forma barata de eludir el problema subyacente.

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


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

Esta es una versión compilable completa de la solución de Johanne 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

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

Solo sentí que se debe indicar 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. EDIT: No, en realidad está ahí en la respuesta de Walter.

Esta función se da:

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

Llámalo con el siguiente fragmento de código:

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

DEMO


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

Pensando en el problema un poco más basado en la respuesta dada, he encontrado otra manera 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 de std::tuple en un paquete de parámetros por derecho propio. call_or_recurse es necesario como una 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 esto sea una solución "mejor", pero es otra forma de pensar y resolverlo.

Como otra solución alternativa, puede usar enable_if , para formar algo 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.


c ++ 14 solución. En primer lugar, algunas placas 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 un 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 permiten trabajar con paquetes de parámetros sin tener que generar nuevas sobrecargas externas.

Por supuesto, en c ++ 17 acabas de

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

relativamente fácil y obtenga la sintaxis más limpia de c ++ 17 lista para ser enviada.

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

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





iterable-unpacking