stringtranslate.com

Metaprogramación de plantillas

La metaprogramación de plantillas ( TMP ) es una técnica de metaprogramación en la que un compilador utiliza plantillas para generar código fuente temporal , que el compilador fusiona con el resto del código fuente y luego compila. La salida de estas plantillas puede incluir constantes de tiempo de compilación , estructuras de datos y funciones completas . El uso de plantillas puede considerarse como polimorfismo de tiempo de compilación . La técnica se utiliza en varios lenguajes, siendo el más conocido C++ , pero también Curl , D , Nim y XL .

La metaprogramación de plantillas fue, en cierto sentido, descubierta accidentalmente. [1] [2]

Algunos otros lenguajes admiten funciones de tiempo de compilación similares, si no más potentes (como las macros Lisp ), pero éstas quedan fuera del alcance de este artículo.

Componentes de la metaprogramación de plantillas

El uso de plantillas como técnica de metaprogramación requiere dos operaciones distintas: se debe definir una plantilla y se debe instanciar una plantilla definida . La forma genérica del código fuente generado se describe en la definición de la plantilla y, cuando se instancia la plantilla, se utiliza la forma genérica en la plantilla para generar un conjunto específico de código fuente.

La metaprogramación de plantillas es Turing-completa , lo que significa que cualquier cálculo expresable por un programa de computadora puede ser calculado, de alguna forma, por un metaprograma de plantilla. [3]

Las plantillas son diferentes de las macros . Una macro es un fragmento de código que se ejecuta en tiempo de compilación y realiza una manipulación textual del código que se va a compilar (por ejemplo, las macros de C++ ) o manipula el árbol de sintaxis abstracta que produce el compilador (por ejemplo, las macros de Rust o Lisp ). Las macros textuales son notablemente más independientes de la sintaxis del lenguaje que se está manipulando, ya que simplemente cambian el texto en memoria del código fuente justo antes de la compilación.

Los metaprogramas de plantilla no tienen variables mutables , es decir, ninguna variable puede cambiar de valor una vez que se ha inicializado, por lo tanto, la metaprogramación de plantilla puede verse como una forma de programación funcional . De hecho, muchas implementaciones de plantilla implementan el control de flujo solo a través de la recursión , como se ve en el ejemplo siguiente.

Utilizando metaprogramación de plantillas

Aunque la sintaxis de la metaprogramación con plantillas suele ser muy diferente de la del lenguaje de programación con el que se utiliza, tiene usos prácticos. Algunas razones comunes para usar plantillas son implementar una programación genérica (evitando secciones de código que sean similares salvo algunas variaciones menores) o realizar una optimización automática en tiempo de compilación, como hacer algo una vez en tiempo de compilación en lugar de cada vez que se ejecuta el programa; por ejemplo, haciendo que el compilador desenrolle los bucles para eliminar saltos y decrementos en el conteo de bucles cada vez que se ejecuta el programa.

Generación de clases en tiempo de compilación

Lo que significa exactamente "programar en tiempo de compilación" se puede ilustrar con un ejemplo de una función factorial, que en C++ sin plantilla se puede escribir usando recursión de la siguiente manera:

factorial sin signo ( n sin signo ) { devolver n == 0 ? 1 : n * factorial ( n - 1 ); }               // Ejemplos de uso: // factorial(0) produciría 1; // factorial(4) produciría 24.

El código anterior se ejecutará en tiempo de ejecución para determinar el valor factorial de los literales 0 y 4. Al utilizar la metaprogramación de plantillas y la especialización de plantillas para proporcionar la condición final para la recursión, los factoriales utilizados en el programa (ignorando cualquier factorial no utilizado) se pueden calcular en tiempo de compilación con este código:

plantilla < unsigned N > struct factorial { static constexpr unsigned valor = N * factorial < N - 1 >:: valor ; };             plantilla <> struct factorial < 0 > { static constexpr valor sin signo = 1 ; };        // Ejemplos de uso: // factorial<0>::value produciría 1; // factorial<4>::value produciría 24.

El código anterior calcula el valor factorial de los literales 0 y 4 en tiempo de compilación y utiliza los resultados como si fueran constantes precalculadas. Para poder utilizar plantillas de esta manera, el compilador debe conocer el valor de sus parámetros en tiempo de compilación, lo que tiene la condición previa natural de que factorial<X>::value solo se puede utilizar si se conoce X en tiempo de compilación. En otras palabras, X debe ser un literal constante o una expresión constante.

En C++11 y C++20 , se introdujeron constexpr y consteval para permitir que el compilador ejecute código. Al utilizar constexpr y consteval, se puede utilizar la definición factorial recursiva habitual con la sintaxis sin plantilla. [4]

Optimización de código en tiempo de compilación

El ejemplo factorial anterior es un ejemplo de optimización de código en tiempo de compilación, ya que todos los factoriales utilizados por el programa se compilan previamente y se inyectan como constantes numéricas en la compilación, lo que ahorra tanto sobrecarga de tiempo de ejecución como consumo de memoria . Sin embargo, se trata de una optimización relativamente menor.

Como otro ejemplo más significativo de desenrollado de bucles en tiempo de compilación , se puede utilizar la metaprogramación de plantillas para crear clases de vectores de longitud n (donde n se conoce en tiempo de compilación). La ventaja sobre un vector de longitud n más tradicional es que los bucles se pueden desenrollar, lo que da como resultado un código muy optimizado. Como ejemplo, considere el operador de suma. Una suma de vectores de longitud n se podría escribir como

plantilla < int longitud > Vector < longitud >& Vector < longitud >:: operador += ( const Vector < longitud >& rhs ) { for ( int i = 0 ; i < longitud ; ++ i ) valor [ i ] += rhs . valor [ i ]; return * this ; }                    

Cuando el compilador crea una instancia de la plantilla de función definida anteriormente, se puede producir el siguiente código: [ cita requerida ]

plantilla <> Vector < 2 >& Vector < 2 >:: operador += ( const Vector < 2 >& rhs ) { valor [ 0 ] += rhs . valor [ 0 ]; valor [ 1 ] += rhs . valor [ 1 ]; return * this ; }             

El optimizador del compilador debería poder desenrollar el forbucle porque el parámetro de plantilla lengthes una constante en el momento de la compilación.

Sin embargo, tenga cuidado y sea cauteloso ya que esto puede provocar una hinchazón del código, ya que se generará un código desenrollado separado para cada 'N' (tamaño del vector) con el que cree una instancia.

Polimorfismo estático

El polimorfismo es una función de programación estándar común donde los objetos derivados se pueden usar como instancias de su objeto base, pero donde se invocarán los métodos de los objetos derivados, como en este código.

clase Base { público : método virtual void () { std :: cout << "Base" ; } virtual ~ Base () {} };            clase Derivado : público Base { público : virtual void método () { std :: cout << "Derivado" ; } };            int main () { Base * pBase = new Derived ; pBase -> método (); //genera "Derived" delete pBase ; retorna 0 ; }            

donde todas las invocaciones de virtualmétodos serán las de la clase más derivada. Este comportamiento polimórfico dinámico se obtiene (normalmente) mediante la creación de tablas de búsqueda virtuales para clases con métodos virtuales, tablas que se recorren en tiempo de ejecución para identificar el método que se va a invocar. Por lo tanto, el polimorfismo en tiempo de ejecución implica necesariamente una sobrecarga de ejecución (aunque en las arquitecturas modernas la sobrecarga es pequeña).

Sin embargo, en muchos casos el comportamiento polimórfico necesario es invariable y se puede determinar en tiempo de compilación. Entonces se puede utilizar el Patrón de Plantilla Curiosamente Recurrente (CRTP) para lograr un polimorfismo estático , que es una imitación del polimorfismo en el código de programación, pero que se resuelve en tiempo de compilación y, por lo tanto, elimina las búsquedas en tablas virtuales en tiempo de ejecución. Por ejemplo:

plantilla < clase Derivado > struct base { void interfaz () { // ... static_cast < Derivado *> ( this ) -> implementación (); // ... } };          estructura derivada : base < derivada > { void implementación () { // ... } };        

Aquí la plantilla de clase base aprovechará el hecho de que los cuerpos de las funciones miembro no se instancian hasta después de sus declaraciones, y utilizará miembros de la clase derivada dentro de sus propias funciones miembro, mediante el uso de un static_cast, generando así en la compilación una composición de objetos con características polimórficas. Como ejemplo de uso en el mundo real, el CRTP se utiliza en la biblioteca de iteradores Boost . [5]

Otro uso similar es el " truco de Barton-Nackman ", a veces denominado "expansión de plantilla restringida", donde la funcionalidad común se puede colocar en una clase base que se utiliza no como un contrato sino como un componente necesario para imponer un comportamiento conforme mientras se minimiza la redundancia del código.

Generación de tabla estática

La ventaja de las tablas estáticas es que permiten reemplazar los cálculos "costosos" por una simple operación de indexación de matrices (para ver ejemplos, consulte la tabla de búsqueda ). En C++, existe más de una forma de generar una tabla estática en tiempo de compilación. La siguiente lista muestra un ejemplo de creación de una tabla muy simple mediante el uso de estructuras recursivas y plantillas variádicas . La tabla tiene un tamaño de diez. Cada valor es el cuadrado del índice.

#include <iostream> #include <matriz>  constexpr int TAMAÑO_DE_TABLA = 10 ;    /** * Plantilla variádica para una estructura auxiliar recursiva. */ plantilla < int INDEX = 0 , int ... D > struct Helper : Helper < INDEX + 1 , D ..., INDEX * INDEX > { };                /** * Especialización de la plantilla para finalizar la recursión cuando el tamaño de la tabla alcanza TABLE_SIZE. */ template < int ... D > struct Helper < TABLE_SIZE , D ... > { static constexpr std :: array < int , TABLE_SIZE > table = { D ... }; };             constexpr std :: array < int , TAMAÑO_DE_TABLA > tabla = Ayudante <>:: tabla ;     enum { CUATRO = tabla [ 2 ] // uso en tiempo de compilación };     int main () { for ( int i = 0 ; i < TAMAÑO_DE_TABLA ; i ++ ) { std :: cout << tabla [ i ] << std :: endl ; // uso en tiempo de ejecución } std :: cout << "CUATRO: " << CUATRO << std :: endl ; }                        

La idea detrás de esto es que el struct Helper hereda recursivamente de un struct con un argumento de plantilla más (en este ejemplo calculado como INDEX * INDEX) hasta que la especialización de la plantilla finaliza la recursión en un tamaño de 10 elementos. La especialización simplemente usa la lista de argumentos de la variable como elementos para la matriz. El compilador producirá un código similar al siguiente (tomado de clang llamado con -Xclang -ast-print -fsyntax-only).

plantilla < int INDEX = 0 , int ... D > struct Ayudante : Ayudante < INDEX + 1 , D ..., INDEX * INDEX > { }; plantilla <> struct Ayudante < 0 , <>> : Ayudante < 0 + 1 , 0 * 0 > { }; plantilla <> struct Ayudante < 1 , < 0 >> : Ayudante < 1 + 1 , 0 , 1 * 1 > { }; plantilla <> struct Ayudante < 2 , < 0 , 1 >> : Ayudante < 2 + 1 , 0 , 1 , 2 * 2 > { }; plantilla <> struct Ayudante < 3 , < 0 , 1 , 4 >> : Ayudante < 3 + 1 , 0 , 1 , 4 , 3 * 3 > { }; plantilla <> struct Ayudante < 4 , < 0 , 1 , 4 , 9 >> : Ayudante < 4 + 1 , 0 , 1 , 4 , 9 , 4 * 4 > { }; plantilla <> struct Ayudante < 5 , < 0 , 1 , 4 , 9 , 16 >> :Ayudante < 5 + 1 , 0 ,                                                                                                     1 , 4 , 9 , 16 , 5 * 5 > { }; plantilla <> estructura Ayudante < 6 , < 0 , 1 , 4 , 9 , 16 , 25 >> : Ayudante < 6 + 1 , 0 , 1 , 4 , 9 , 16 , 25 , 6 * 6 > { }; plantilla <> estructura Ayudante < 7 , < 0 , 1 , 4 , 9 , 16 , 25 , 36 >> : Ayudante < 7 + 1 , 0 , 1 , 4 , 9 , 16 , 25 , 36 , 7 * 7 > { }; plantilla <> estructura Ayudante < 8 , < 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 >> : Ayudante < 8 + 1 , 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 8 * 8 > { }; plantilla <> estructura Ayudante < 9 , < 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 64 >> : Ayudante < 9 + 1 , 0 , 1 , 4 , 9 , 16 , 25                                                                                                    , 36 , 49 , 64 , 9 * 9 > { }; plantilla <> estructura Ayudante < 10 , < 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 64 , 81 >> { static constexpr std :: matriz < int , TAMAÑO_TABLA > tabla = { 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 64 , 81 }; };                                    

Desde C++17 esto se puede escribir de forma más legible así:

 #include <iostream> #include <matriz>  constexpr int TAMAÑO_DE_TABLA = 10 ;    constexpr std :: array < int , TAMAÑO_DE_TABLA > tabla = [] { // O: constexpr auto tabla std :: array < int , TAMAÑO_DE_TABLA > A = {}; para ( sin signo i = 0 ; i < TAMAÑO_DE_TABLA ; i ++ ) { A [ i ] = i * i ; } devolver A ; }();                              enum { CUATRO = tabla [ 2 ] // uso en tiempo de compilación };     int main () { for ( int i = 0 ; i < TAMAÑO_DE_TABLA ; i ++ ) { std :: cout << tabla [ i ] << std :: endl ; // uso en tiempo de ejecución } std :: cout << "CUATRO: " << CUATRO << std :: endl ; }                        

Para mostrar un ejemplo más sofisticado, el código en la siguiente lista se ha ampliado para tener un ayudante para el cálculo de valores (en preparación para cálculos más complicados), un desplazamiento específico de la tabla y un argumento de plantilla para el tipo de valores de la tabla (por ejemplo, uint8_t, uint16_t, ...).

 #include <iostream> #include <matriz>  constexpr int TAMAÑO_TABLA = 20 ; constexpr int DESPLAZAMIENTO = 12 ;        /** * Plantilla para calcular una sola entrada de tabla */ template < typename VALUETYPE , VALUETYPE OFFSET , VALUETYPE INDEX > struct ValueHelper { static constexpr VALUETYPE value = OFFSET + INDEX * INDEX ; };                  /** * Plantilla variádica para una estructura auxiliar recursiva. */ plantilla < typename VALUETYPE , VALUETYPE OFFSET , int N = 0 , VALUETYPE ... D > struct Helper : Helper < VALUETYPE , OFFSET , N + 1 , D ..., ValueHelper < VALUETYPE , OFFSET , N >:: value > { };                    /** * Especialización de la plantilla para finalizar la recursión cuando el tamaño de la tabla alcanza TABLE_SIZE. */ template < typename VALUETYPE , VALUETYPE OFFSET , VALUETYPE ... D > struct Helper < VALUETYPE , OFFSET , TABLE_SIZE , D ... > { static constexpr std :: array < VALUETYPE , TABLE_SIZE > table = { D ... }; };                   constexpr std :: matriz < uint16_t , TAMAÑO_TABLA > tabla = Ayudante < uint16_t , DESPLAZAMIENTO >:: tabla ;      int main () { para ( int i = 0 ; i < TAMAÑO_DE_TABLA ; i ++ ) { std :: cout << tabla [ i ] << std :: endl ; } }                  

Lo cual podría escribirse de la siguiente manera usando C++17:

#include <iostream> #include <matriz>  constexpr int TAMAÑO_TABLA = 20 ; constexpr int DESPLAZAMIENTO = 12 ;        plantilla < typename TIPO_VALOR , int DESPLAZAMIENTO > constexpr std :: matriz < TIPO_VALOR , TAMAÑO_TABLA > tabla = [] { // O: constexpr auto tabla std :: matriz < TIPO_VALOR , TAMAÑO_TABLA > A = {}; para ( sin signo i = 0 ; i < TAMAÑO_TABLA ; i ++ ) { A [ i ] = DESPLAZAMIENTO + i * i ; } devolver A ; }();                                   int main () { para ( int i = 0 ; i < TAMAÑO_DE_TABLA ; i ++ ) { std :: cout << tabla < uint16_t , DESPLAZAMIENTO > [ i ] << std :: endl ; } }                   

Conceptos

El estándar C++20 trajo a los programadores de C++ una nueva herramienta para la programación de metaplantillas y conceptos. [6]

Los conceptos permiten a los programadores especificar requisitos para el tipo, a fin de hacer posible la creación de instancias de plantillas. El compilador busca una plantilla con el concepto que tenga los requisitos más exigentes.

A continuación se muestra un ejemplo del famoso problema del zumbido Fizz resuelto con la Programación Meta de Plantillas.

#include <boost/type_index.hpp> // para una impresión bonita de tipos #include <iostream> #include <tuple>   /** * Representación de tipo de palabras a imprimir */ struct Fizz {}; struct Buzz {}; struct FizzBuzz {}; plantilla < size_t _N > struct number { constexpr static size_t N = _N ; };                 /** * Conceptos usados ​​para definir condiciones para especializaciones */ plantilla < typename Any > concepto has_N = requiere { requiere Any :: N - Any :: N == 0 ; }; plantilla < typename A > concepto fizz_c = has_N < A > && requiere { requiere A :: N % 3 == 0 ; }; plantilla < typename A > concepto buzz_c = has_N < A > && requiere { requiere A :: N % 5 == 0 ;}; plantilla < typename A > concepto fizzbuzz_c = fizz_c < A > && buzz_c < A > ;                                              /** * Al especializar la estructura `res`, con los requisitos de los conceptos, se realiza la instanciación adecuada */ plantilla < typename X > struct res ; plantilla < fizzbuzz_c X > struct res < X > { using result = FizzBuzz ; }; plantilla < fizz_c X > struct res < X > { using result = Fizz ; }; plantilla < buzz_c X > struct res < X > { using result = Buzz ; }; plantilla < has_N X > struct res < X > { using result = X ; };                                       /** * Predeclaración del concatenador */ plantilla < size_t cnt , typename ... Args > struct concatenator ;      /** * Forma recursiva de concatenar los siguientes tipos */ plantilla < tamaño_t cnt , nombre_tipo ... Args > struct concatenador < cnt , std :: tupla < Args ... >> { using type = nombre_tipo concatenador < cnt - 1 , std :: tupla < nombre_tipo res < número < cnt > >:: resultado , Args ... >>:: tipo ;};                      /** * Caso base */ plantilla < typename ... Args > concatenador de estructuras < 0 , std :: tupla < Args ... >> { using type = std :: tupla < Args ... > ;};          /** * Obtenedor del resultado final */ plantilla < size_t Cantidad > usando fizz_buzz_full_template = typename concatenador < Cantidad - 1 , std :: tupla < typename res < número < Cantidad >>:: resultado >>:: tipo ;         int main () { // imprimir el resultado con boost, para que quede claro std :: cout << boost :: typeindex :: type_id < fizz_buzz_full_template < 100 >> (). pretty_name () << std :: endl ; /* Resultado: std::tuple<número<1ul>, número<2ul>, Fizz, número<4ul>, Buzz, Fizz, número<7ul>, número<8ul>, Fizz, Buzz, número<11ul>, Fizz, número<13ul>, número<14ul>, FizzBuzz, número<16ul>, número<17ul>, Fizz, número<19ul>, Buzz, Fizz, número<22ul>, número<23ul>, Fizz, Buzz, número<26ul>, Fizz, número<28ul>, número<29ul>, FizzBuzz, número<31ul>, número<32ul>, Fizz, número<34ul>, Buzz, Fizz, número<37ul>, número<38ul>, Fizz, Buzz, número<41ul>, Fizz, número<43ul>, número<44ul>, FizzBuzz, número<46ul>, número<47ul>, Fizz, número<49ul>, Buzz, Fizz, número<52ul>, número<53ul>, Fizz, Buzz, número<56ul>, Fizz, número<58ul>, número<59ul>, FizzBuzz, número<61ul>, número<62ul>, Fizz, número<64ul>, Buzz, Fizz, número<67ul>, número<68ul>, Fizz, Buzz, número<71ul>, Fizz, número<73ul>, número<74ul>, FizzBuzz, número<76ul>, número<77ul>, Fizz, número<79ul>, Buzz, Fizz, número<82ul>, número<83ul>, Fizz, Buzz, número<86ul>, Fizz, número<88ul>, número<89ul>, FizzBuzz, número<91ul>, número<92ul>, Fizz, número<94ul>, Buzz, Fizz, número<97ul>, número<98ul>, Fizz, Buzz> */ }     

Ventajas y desventajas de la metaprogramación de plantillas

Las compensaciones entre el tiempo de compilación y el tiempo de ejecución se hacen visibles si se utiliza una gran cantidad de metaprogramación de plantillas.

Véase también

Referencias

  1. ^ Scott Meyers (12 de mayo de 2005). Effective C++: 55 maneras específicas de mejorar sus programas y diseños. Pearson Education. ISBN 978-0-13-270206-5.
  2. ^ Ver Historia de TMP en Wikilibros
  3. ^ Veldhuizen, Todd L. (2003). "Las plantillas de C++ son Turing completas". CiteSeerX 10.1.1.14.3670 . 
  4. ^ "Constexpr - Expresiones constantes generalizadas en C++11 - Cprogramming.com". www.cprogramming.com .
  5. ^ "Fachada del iterador - 1.79.0".
  6. ^ "Restricciones y conceptos (desde C++20) - cppreference.com". es.cppreference.com .
  7. ^ Czarnecki, K.; O'Donnell, J.; Striegnitz, J.; Taha, Walid Mohamed (2004). "Implementación de DSL en metaocaml, template haskell y C++" (PDF) . Universidad de Waterloo, Universidad de Glasgow, Centro de investigación Julich, Universidad Rice. La metaprogramación de plantillas de C++ sufre una serie de limitaciones, incluidos problemas de portabilidad debido a las limitaciones del compilador (aunque esto ha mejorado significativamente en los últimos años), falta de soporte de depuración o IO durante la instanciación de plantillas, largos tiempos de compilación, errores largos e incomprensibles, mala legibilidad del código y malos informes de errores.
  8. ^ Sheard, Tim; Jones, Simon Peyton (2002). "Template Meta-programming for Haskell" (PDF) . ACM 1-58113-415-0/01/0009. El provocativo artículo de Robinson identifica las plantillas de C++ como un éxito importante, aunque accidental, del diseño del lenguaje C++. A pesar de la naturaleza extremadamente barroca de la metaprogramación de plantillas, las plantillas se utilizan de formas fascinantes que van más allá de los sueños más descabellados de los diseñadores del lenguaje. Tal vez sea sorprendente que, en vista del hecho de que las plantillas son programas funcionales, los programadores funcionales hayan tardado en capitalizar el éxito de C++.

Enlaces externos