template c++ c++11 iteration variadic-templates template-meta-programming

c++ - template - ¿Cómo hacer cálculos genéricos sobre paquetes de argumentos heterogéneos de una función de plantilla variadica?



variadic c++ (5)

Como no estaba contento con lo que encontré, traté de encontrar una solución y terminé escribiendo una pequeña biblioteca que permite formular operaciones genéricas en los paquetes de argumentos. Mi solución tiene las siguientes características:

  • Permite iterar sobre todos o algunos elementos de un paquete de argumentos, posiblemente especificado al calcular sus índices en el paquete;
  • Permite reenviar porciones computadas de un paquete de argumentos a functors variadic;
  • Solo requiere incluir un archivo de encabezado relativamente corto;
  • Hace un uso extensivo del reenvío perfecto para permitir una fuerte entrada y evita copias / movimientos innecesarios para permitir una pérdida mínima de rendimiento;
  • La implementación interna de los algoritmos de iteración se basa en la optimización de la clase base vacía para minimizar el consumo de memoria;
  • Es fácil (relativamente, considerando su meta-programación de plantillas) extenderse y adaptarse.

Primero mostraré qué se puede hacer con la biblioteca y luego publicaré su implementación .

CASOS DE USO

Aquí hay un ejemplo de cómo se puede usar la función for_each_in_arg_pack() para iterar a través de todos los argumentos de un paquete y pasar cada argumento en entrada a algún functor proporcionado por el cliente (por supuesto, el funtor debe tener un operador de llamada genérico si el argumento el paquete contiene valores de tipos heterogéneos):

// Simple functor with a generic call operator that prints its input. This is used by the // following functors and by some demonstrative test cases in the main() routine. struct print { template<typename T> void operator () (T&& t) { cout << t << endl; } }; // This shows how a for_each_*** helper can be used inside a variadic template function template<typename... Ts> void print_all(Ts&&... args) { for_each_in_arg_pack(print(), forward<Ts>(args)...); }

El functor de print anterior también se puede usar en cálculos más complejos. En particular, aquí es cómo uno iteraría en un subconjunto (en este caso, un sub-rango ) de los argumentos en un paquete:

// Shows how to select portions of an argument pack and // invoke a functor for each of the selected elements template<typename... Ts> void split_and_print(Ts&&... args) { constexpr size_t packSize = sizeof...(args); constexpr size_t halfSize = packSize / 2; cout << "Printing first half:" << endl; for_each_in_arg_pack_subset( print(), // The functor to invoke for each element index_range<0, halfSize>(), // The indices to select forward<Ts>(args)... // The argument pack ); cout << "Printing second half:" << endl; for_each_in_arg_pack_subset( print(), // The functor to invoke for each element index_range<halfSize, packSize>(), // The indices to select forward<Ts>(args)... // The argument pack ); }

A veces, uno puede querer reenviar una parte de un paquete de argumentos a otro functor variadic en lugar de iterar a través de sus elementos y pasar cada uno de ellos individualmente a un funtor no variadic. Esto es lo que permite el algoritmo forward_subpack() :

// Functor with variadic call operator that shows the usage of for_each_*** // to print all the arguments of a heterogeneous pack struct my_func { template<typename... Ts> void operator ()(Ts&&... args) { print_all(forward<Ts>(args)...); } }; // Shows how to forward only a portion of an argument pack // to another variadic functor template<typename... Ts> void split_and_print(Ts&&... args) { constexpr size_t packSize = sizeof...(args); constexpr size_t halfSize = packSize / 2; cout << "Printing first half:" << endl; forward_subpack(my_func(), index_range<0, halfSize>(), forward<Ts>(args)...); cout << "Printing second half:" << endl; forward_subpack(my_func(), index_range<halfSize, packSize>(), forward<Ts>(args)...); }

Para tareas más específicas, por supuesto es posible recuperar argumentos específicos en un paquete al indexarlos . Esto es lo que permite la función nth_value_of() , junto con sus ayudantes first_value_of() y last_value_of() :

// Shows that arguments in a pack can be indexed template<unsigned I, typename... Ts> void print_first_last_and_indexed(Ts&&... args) { cout << "First argument: " << first_value_of(forward<Ts>(args)...) << endl; cout << "Last argument: " << last_value_of(forward<Ts>(args)...) << endl; cout << "Argument #" << I << ": " << nth_value_of<I>(forward<Ts>(args)...) << endl; }

Si el paquete de argumentos es homogéneo, por otro lado (es decir, todos los argumentos tienen el mismo tipo), una formulación como la siguiente podría ser preferible. La meta-función is_homogeneous_pack<> permite determinar si todos los tipos en un paquete de parámetros son homogéneos, y está destinado principalmente a ser utilizado en las static_assert() :

// Shows the use of range-based for loops to iterate over a // homogeneous argument pack template<typename... Ts> void print_all(Ts&&... args) { static_assert( is_homogeneous_pack<Ts...>::value, "Template parameter pack not homogeneous!" ); for (auto&& x : { args... }) { // Do something with x... } cout << endl; }

Finalmente, dado que las lambdas son simplemente azúcar sintáctica para los funtores, también se pueden usar en combinación con los algoritmos anteriores; sin embargo, hasta que las versiones genéricas de lambdas sean compatibles con C ++, esto solo es posible para paquetes de argumentos homogéneos . El siguiente ejemplo también muestra el uso de la meta-función homogeneous-type<> , que devuelve el tipo de todos los argumentos en un paquete homogéneo:

// ... static_assert( is_homogeneous_pack<Ts...>::value, "Template parameter pack not homogeneous!" ); using type = homogeneous_type<Ts...>::type; for_each_in_arg_pack([] (type const& x) { cout << x << endl; }, forward<Ts>(args)...);

Esto es básicamente lo que la biblioteca permite hacer, pero creo que incluso podría extenderse para llevar a cabo tareas más complejas.

IMPLEMENTACIÓN

Ahora viene la implementación, que es un poco complicada en sí misma, así que me basaré en los comentarios para explicar el código y evitar hacer que esta publicación sea demasiado larga (quizás ya lo sea):

#include <type_traits> #include <utility> //=============================================================================== // META-FUNCTIONS FOR EXTRACTING THE n-th TYPE OF A PARAMETER PACK // Declare primary template template<int I, typename... Ts> struct nth_type_of { }; // Base step template<typename T, typename... Ts> struct nth_type_of<0, T, Ts...> { using type = T; }; // Induction step template<int I, typename T, typename... Ts> struct nth_type_of<I, T, Ts...> { using type = typename nth_type_of<I - 1, Ts...>::type; }; // Helper meta-function for retrieving the first type in a parameter pack template<typename... Ts> struct first_type_of { using type = typename nth_type_of<0, Ts...>::type; }; // Helper meta-function for retrieving the last type in a parameter pack template<typename... Ts> struct last_type_of { using type = typename nth_type_of<sizeof...(Ts) - 1, Ts...>::type; }; //=============================================================================== // FUNCTIONS FOR EXTRACTING THE n-th VALUE OF AN ARGUMENT PACK // Base step template<int I, typename T, typename... Ts> auto nth_value_of(T&& t, Ts&&... args) -> typename std::enable_if<(I == 0), decltype(std::forward<T>(t))>::type { return std::forward<T>(t); } // Induction step template<int I, typename T, typename... Ts> auto nth_value_of(T&& t, Ts&&... args) -> typename std::enable_if<(I > 0), decltype( std::forward<typename nth_type_of<I, T, Ts...>::type>( std::declval<typename nth_type_of<I, T, Ts...>::type>() ) )>::type { using return_type = typename nth_type_of<I, T, Ts...>::type; return std::forward<return_type>(nth_value_of<I - 1>((std::forward<Ts>(args))...)); } // Helper function for retrieving the first value of an argument pack template<typename... Ts> auto first_value_of(Ts&&... args) -> decltype( std::forward<typename first_type_of<Ts...>::type>( std::declval<typename first_type_of<Ts...>::type>() ) ) { using return_type = typename first_type_of<Ts...>::type; return std::forward<return_type>(nth_value_of<0>((std::forward<Ts>(args))...)); } // Helper function for retrieving the last value of an argument pack template<typename... Ts> auto last_value_of(Ts&&... args) -> decltype( std::forward<typename last_type_of<Ts...>::type>( std::declval<typename last_type_of<Ts...>::type>() ) ) { using return_type = typename last_type_of<Ts...>::type; return std::forward<return_type>(nth_value_of<sizeof...(Ts) - 1>((std::forward<Ts>(args))...)); } //=============================================================================== // METAFUNCTION FOR COMPUTING THE UNDERLYING TYPE OF HOMOGENEOUS PARAMETER PACKS // Used as the underlying type of non-homogeneous parameter packs struct null_type { }; // Declare primary template template<typename... Ts> struct homogeneous_type; // Base step template<typename T> struct homogeneous_type<T> { using type = T; static const bool isHomogeneous = true; }; // Induction step template<typename T, typename... Ts> struct homogeneous_type<T, Ts...> { // The underlying type of the tail of the parameter pack using type_of_remaining_parameters = typename homogeneous_type<Ts...>::type; // True if each parameter in the pack has the same type static const bool isHomogeneous = std::is_same<T, type_of_remaining_parameters>::value; // If isHomogeneous is "false", the underlying type is the fictitious null_type using type = typename std::conditional<isHomogeneous, T, null_type>::type; }; // Meta-function to determine if a parameter pack is homogeneous template<typename... Ts> struct is_homogeneous_pack { static const bool value = homogeneous_type<Ts...>::isHomogeneous; }; //=============================================================================== // META-FUNCTIONS FOR CREATING INDEX LISTS // The structure that encapsulates index lists template <unsigned... Is> struct index_list { }; // Collects internal details for generating index ranges [MIN, MAX) namespace detail { // Declare primary template for index range builder template <unsigned MIN, unsigned N, unsigned... Is> struct range_builder; // Base step template <unsigned MIN, unsigned... Is> struct range_builder<MIN, MIN, Is...> { typedef index_list<Is...> type; }; // Induction step template <unsigned MIN, unsigned N, unsigned... Is> struct range_builder : public range_builder<MIN, N - 1, N - 1, Is...> { }; } // Meta-function that returns a [MIN, MAX) index range template<unsigned MIN, unsigned MAX> using index_range = typename detail::range_builder<MIN, MAX>::type; //=============================================================================== // CLASSES AND FUNCTIONS FOR REALIZING LOOPS ON ARGUMENT PACKS // Implementation inspired by @jogojapan''s answer to this question: // http://stackoverflow.com/questions/14089637/return-several-arguments-for-another-function-by-a-single-function // Collects internal details for implementing functor invocation namespace detail { // Functor invocation is realized through variadic inheritance. // The constructor of each base class invokes an input functor. // An functor invoker for an argument pack has one base class // for each argument in the pack // Realizes the invocation of the functor for one parameter template<unsigned I, typename T> struct invoker_base { template<typename F, typename U> invoker_base(F&& f, U&& u) { f(u); } }; // Necessary because a class cannot inherit the same class twice template<unsigned I, typename T> struct indexed_type { static const unsigned int index = I; using type = T; }; // The functor invoker: inherits from a list of base classes. // The constructor of each of these classes invokes the input // functor with one of the arguments in the pack. template<typename... Ts> struct invoker : public invoker_base<Ts::index, typename Ts::type>... { template<typename F, typename... Us> invoker(F&& f, Us&&... args) : invoker_base<Ts::index, typename Ts::type>(std::forward<F>(f), std::forward<Us>(args))... { } }; } // The functor provided in the first argument is invoked for each // argument in the pack whose index is contained in the index list // specified in the second argument template<typename F, unsigned... Is, typename... Ts> void for_each_in_arg_pack_subset(F&& f, index_list<Is...> const& i, Ts&&... args) { // Constructors of invoker''s sub-objects will invoke the functor. // Note that argument types must be paired with numbers because the // implementation is based on inheritance, and one class cannot // inherit the same base class twice. detail::invoker<detail::indexed_type<Is, typename nth_type_of<Is, Ts...>::type>...> invoker( f, (nth_value_of<Is>(std::forward<Ts>(args)...))... ); } // The functor provided in the first argument is invoked for each // argument in the pack template<typename F, typename... Ts> void for_each_in_arg_pack(F&& f, Ts&&... args) { for_each_in_arg_pack_subset(f, index_range<0, sizeof...(Ts)>(), std::forward<Ts>(args)...); } // The functor provided in the first argument is given in input the // arguments in whose index is contained in the index list specified // as the second argument. template<typename F, unsigned... Is, typename... Ts> void forward_subpack(F&& f, index_list<Is...> const& i, Ts&&... args) { f((nth_value_of<Is>(std::forward<Ts>(args)...))...); } // The functor provided in the first argument is given in input all the // arguments in the pack. template<typename F, typename... Ts> void forward_pack(F&& f, Ts&&... args) { f(std::forward<Ts>(args)...); }

CONCLUSIÓN

Por supuesto, aunque proporcioné mi propia respuesta a esta pregunta (y en realidad debido a este hecho), tengo curiosidad por saber si existen soluciones alternativas o mejores que me he perdido, aparte de las mencionadas en la sección "Trabajos relacionados". de la pregunta.

PREMISA:

Después de jugar un poco con las plantillas variadic un poco, me di cuenta de que lograr algo que vaya un poco más allá de las tareas de meta-programación triviales pronto se vuelve bastante engorroso. En particular, me encontré deseando una forma de realizar operaciones genéricas sobre un paquete de argumentos como iterar , dividir , std::for_each manera std::for_each , y así sucesivamente.

Después de ver esta conferencia de Andrei Alexandrescu de C ++ y Beyond 2012 sobre la conveniencia de la static if en C ++ (una construcción tomada del Lenguaje de programación D ) tuve la sensación de que algún tipo de static for sería útil, y me siento más de estos constructos static podría traer beneficio.

Así que comencé a preguntarme si hay una forma de lograr algo como esto para los paquetes de argumentos de una función de plantilla variadica ( pseudo-código ):

template<typename... Ts> void my_function(Ts&&... args) { static for (int i = 0; i < sizeof...(args); i++) // PSEUDO-CODE! { foo(nth_value_of<i>(args)); } }

Que se traduciría en tiempo de compilación a algo como esto:

template<typename... Ts> void my_function(Ts&&... args) { foo(nth_value_of<0>(args)); foo(nth_value_of<1>(args)); // ... foo(nth_value_of<sizeof...(args) - 1>(args)); }

En principio, static_for permitiría un procesamiento aún más elaborado:

template<typename... Ts> void foo(Ts&&... args) { constexpr s = sizeof...(args); static for (int i = 0; i < s / 2; i++) { // Do something foo(nth_value_of<i>(args)); } static for (int i = s / 2; i < s; i++) { // Do something different bar(nth_value_of<i>(args)); } }

O para una expresión más expresiva como esta:

template<typename... Ts> void foo(Ts&&... args) { static for_each (auto&& x : args) { foo(x); } }

TRABAJO RELACIONADO:

Hice una búsqueda en la Web y descubrí que algo existe realmente:

  • Este enlace describe cómo convertir un paquete de parámetros en un vector Boost.MPL, pero eso solo llega a la mitad (si no menos) hacia el objetivo;
  • esta pregunta sobre SO parece requerir una característica de meta-programación similar y ligeramente relacionada (dividir un paquete de argumentos en dos mitades) - de hecho, hay varias preguntas sobre SO que parecen estar relacionadas con este tema, pero ninguna de las respuestas he leído lo resuelve satisfactoriamente en mi humilde opinión;
  • Boost.Fusion define algoritmos para convertir un paquete de argumentos en una tupla , pero yo preferiría:
    1. no crear temporarios innecesarios para contener argumentos que pueden (y deberían ser) remitidos perfectamente a algunos algoritmos genéricos;
    2. tener una biblioteca pequeña e independiente para hacer eso, mientras que Boost.Fusion probablemente incluirá mucho más material de lo necesario para solucionar este problema.

PREGUNTA:

¿Existe una manera relativamente simple, posiblemente a través de meta-programación de plantillas, para lograr lo que estoy buscando sin incurrir en las limitaciones de los enfoques existentes?


Déjame publicar este código, basado en la discusión:

#include <initializer_list> #define EXPAND(EXPR) std::initializer_list<int>{((EXPR),0)...} // Example of use: #include <iostream> #include <utility> void print(int i){std::cout << "int: " << i << ''/n'';} int print(double d){std::cout << "double: " << d << ''/n'';return 2;} template<class...T> void f(T&&...args){ EXPAND(print(std::forward<T>(args))); } int main(){ f(); f(1,2.,3); }

Comprobé el código generado con g++ -std=c++11 -O1 y main solo contiene 3 llamadas para print , no hay rastro de los ayudantes de expansión.


Después de leer algunas otras publicaciones y retoques por un tiempo, se me ocurrió lo siguiente (algo similar a lo anterior, pero la implementación es un poco diferente). Lo escribí usando el compilador de Visual Studio 2013.

Uso usando una expresión lambda -

static_for_each()( [](std::string const& str) { std::cout << str << std::endl; }, "Hello, ", "Lambda!");

La desventaja cuando se usa una lambda es que los parámetros deben ser del mismo tipo declarados en la lista de parámetros de lambda. Esto significa que solo funcionará con un tipo. Si desea utilizar una función de plantilla, puede usar el siguiente ejemplo.

Uso mediante el uso del functor struct wrapper

struct print_wrapper { template <typename T> void operator()(T&& str) { std::cout << str << " "; } }; // // A little test object we can use. struct test_object { test_object() : str("I''m a test object!") {} std::string str; }; std::ostream& operator<<(std::ostream& os, test_object t) { os << t.str; return os; } // // prints: "Hello, Functor! 1 2 I''m a test object!" static_for_each()(print_wrapper(), "Hello,", "Functor!", 1, 2.0f, test_object());

Esto le permite pasar cualquier tipo que desee y operar con el functor. Encontré esto bastante limpio y funciona muy bien para lo que quería. También puede usarlo con un paquete de parámetros de funciones como este -

template <typename T, typename... Args> void call(T f, Args... args) { static_for_each()(f, args...); } call(print_wrapper(), "Hello", "Call", "Wrapper!");

Aquí está la implementación -

// // Statically iterate over a parameter pack // and call a functor passing each argument. struct static_for_each { private: // // Get the parameter pack argument at index i. template <size_t i, typename... Args> static auto get_arg(Args&&... as) -> decltype(std::get<i>(std::forward_as_tuple(std::forward<Args>(as)...))) { return std::get<i>(std::forward_as_tuple(std::forward<Args>(as)...)); } // // Recursive template for iterating over // parameter pack and calling the functor. template <size_t Start, size_t End> struct internal_static_for { template <typename Functor, typename... Ts> void operator()(Functor f, Ts&&... args) { f(get_arg<Start>(args...)); internal_static_for<Start + 1, End>()(f, args...); } }; // // Specialize the template to end the recursion. template <size_t End> struct internal_static_for<End, End> { template <typename Functor, typename... Ts> void operator()(Functor f, Ts&&... args){} }; public: // // Publically exposed operator()(). // Handles template recursion over parameter pack. // Takes the functor to be executed and a parameter // pack of arguments to pass to the functor, one at a time. template<typename Functor, typename... Ts> void operator()(Functor f, Ts&&... args) { // // Statically iterate over parameter // pack from the first argument to the // last, calling functor f with each // argument in the parameter pack. internal_static_for<0u, sizeof...(Ts)>()(f, args...); } };

Espero que la gente lo encuentre útil :-)


La ... notación tiene algunas opciones interesantes, como:

template<typename T> int print(const T& x) { std::cout << "<" << x << ">"; return 0; } void pass(...) {} template<typename... TS> void printall(TS... ts){ pass(print(ts)...); }

Desafortunadamente, no conozco ninguna forma de hacer cumplir el orden en que se llaman las funciones de impresión (inverso, en mi compilador). Tenga en cuenta que la impresión debe devolver algo.

Este truco puede ser útil si no te importa el orden.


Usando una solución de enumerar (ala Python).

Uso:

void fun(int i, size_t index, size_t size) { if (index != 0) { std::cout << ", "; } std::cout << i; if (index == size - 1) { std::cout << "/n"; } } // fun enumerate(fun, 2, 3, 4); // Expected output: "2, 3, 4/n" // check it at: http://liveworkspace.org/code/1cydbw$4

Código:

// Fun: expects a callable of 3 parameters: Arg, size_t, size_t // Arg: forwarded argument // size_t: index of current argument // size_t: number of arguments template <typename Fun, typename... Args, size_t... Is> void enumerate_impl(Fun&& fun, index_list<Is...>, Args&&... args) { std::initializer_list<int> _{ (fun(std::forward<Args>(args), Is, sizeof...(Is)), 0)... }; (void)_; // placate compiler, only the side-effects interest us } template <typename Fun, typename... Args> void enumerate(Fun&& fun, Args&&... args) { enumerate_impl(fun, index_range<0, sizeof...(args)>(), std::forward<Args>(args)...); }

El generador de rango (pilferred de su solución):

// The structure that encapsulates index lists template <size_t... Is> struct index_list { }; // Collects internal details for generating index ranges [MIN, MAX) namespace detail { // Declare primary template for index range builder template <size_t MIN, size_t N, size_t... Is> struct range_builder; // Base step template <size_t MIN, size_t... Is> struct range_builder<MIN, MIN, Is...> { typedef index_list<Is...> type; }; // Induction step template <size_t MIN, size_t N, size_t... Is> struct range_builder : public range_builder<MIN, N - 1, N - 1, Is...> { }; } // Meta-function that returns a [MIN, MAX) index range template<size_t MIN, size_t MAX> using index_range = typename detail::range_builder<MIN, MAX>::type;