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 lo compila. El resultado de estas plantillas puede incluir constantes en tiempo de compilación , estructuras de datos y funciones completas . El uso de plantillas puede considerarse como un polimorfismo en tiempo de compilación . La técnica es utilizada por varios lenguajes, el más conocido es 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 están 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 crear una instancia de una plantilla definida . La forma genérica del código fuente generado se describe en la definición de la plantilla y, cuando se crea una instancia de la plantilla, la forma genérica de la plantilla se utiliza 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 calcularse, de alguna forma, mediante 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 manipulación textual del código que se va a compilar (por ejemplo, macros de C++ ) o manipula el árbol de sintaxis abstracta que produce el compilador (por ejemplo, macros de Rust o Lisp ). Las macros textuales son notablemente más independientes de la sintaxis del lenguaje que se manipula, 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 plantillas implementan el control de flujo solo mediante recursividad , como se ve en el siguiente ejemplo.

Usando metaprogramación de plantillas

Aunque la sintaxis de la metaprogramación de plantillas suele ser muy diferente del lenguaje de programación con el que se utiliza, tiene usos prácticos. Algunas razones comunes para usar plantillas son implementar programación genérica (evitando secciones de código que sean similares excepto por algunas variaciones menores) o realizar 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 bucles para eliminar saltos y disminuciones en el recuento 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 recursividad de la siguiente manera:

factorial sin signo ( n sin signo ) { retorno n == 0 ? 1 : n * factorial ( n - 1 ); }               // Ejemplos de uso: // factorial(0) produciría 1; // factorial(4) daría como resultado 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 metaprogramación de plantilla y especialización de plantilla para proporcionar la condición final para la recursividad, los factoriales utilizados en el programa (ignorando cualquier factorial no utilizado) pueden calcularse en tiempo de compilación mediante este código:

plantilla < N sin signo > estructura factorial { constexpr estático valor sin signo = N * factorial < N - 1 >:: valor ; };             plantilla <> estructura factorial < 0 > { valor sin signo constexpr estático = 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 el momento de la 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 el momento de la compilación, lo que tiene la condición previa natural de que factorial<X>::value solo se puede utilizar si se conoce X en el momento de la 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. Usando constexpr y consteval, se puede usar la definición factorial recursiva habitual con la sintaxis sin plantilla. [4]

Optimización del 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 en el sentido de que todos los factoriales utilizados por el programa se precompilan y se inyectan como constantes numéricas en la compilación, lo que ahorra tanto la sobrecarga del tiempo de ejecución como la huella de memoria . Sin embargo, se trata de una optimización relativamente menor.

Como otro ejemplo más significativo de desenrollado de bucle en tiempo de compilación , la metaprogramación de plantillas se puede utilizar para crear clases vectoriales 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 podría escribirse como

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

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

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

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 tenga cuidado, ya que esto puede causar un exceso de código, ya que se generará un código desenrollado separado para cada 'N' (tamaño de 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 { public : método virtual void () { std :: cout << "Base" ; } virtual ~ Base () {} };            clase Derivada : Base pública { pública : método virtual void () { std :: cout << "Derivada" ; } };            int main () { Base * pBase = nuevo Derivado ; pBase -> método (); // salidas "Derivadas" eliminar pBase ; devolver 0 ; }            

donde todas las invocaciones de virtualmétodos serán las de la clase más derivada. Este comportamiento dinámicamente polimórfico 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 invariante y puede determinarse en tiempo de compilación. Luego, el patrón de plantilla curiosamente recurrente (CRTP) se puede utilizar para lograr 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 de tablas virtuales en tiempo de ejecución. Por ejemplo:

plantilla < clase Derivada > struct base { void interface () { // ... static_cast < Derivada *> ( esto ) -> implementación (); // ... } };          estructura derivada : base <derivada> { implementación vacía ( ) { // ... } };        

Aquí, la plantilla de clase base aprovechará el hecho de que no se crean instancias de los cuerpos de las funciones miembro hasta después de sus declaraciones, y utilizará miembros de la clase derivada dentro de sus propias funciones miembro, mediante el uso de a static_cast, generando así un objeto en la compilación. composición con características polimórficas. Como ejemplo de uso en el mundo real, 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 no se usa como un contrato sino como un componente necesario para imponer un comportamiento conforme y al mismo tiempo minimizar Redundancia de código.

Generación de tablas estáticas

El beneficio de las tablas estáticas es la sustitución de cálculos "caros" 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 cómo crear una tabla muy simple usando estructuras recursivas y plantillas variadas . La mesa tiene un tamaño de diez. Cada valor es el cuadrado del índice.

#incluye <iostream> #incluye <matriz>  constexpr int TABLE_SIZE = 10 ;    /** * Plantilla variada para una estructura auxiliar recursiva. */ plantilla < int ÍNDICE = 0 , int ... D > struct Ayudante : Ayudante < ÍNDICE + 1 , D ..., ÍNDICE * ÍNDICE > { };                /** * Especialización de la plantilla para finalizar la recursividad cuando el tamaño de la tabla alcance TABLE_SIZE. */ plantilla < int ... D > estructura auxiliar < TABLE_SIZE , D ... > { static constexpr std :: matriz < int , TABLE_SIZE > tabla = { D ... }; };             constexpr std :: matriz < int , TABLE_SIZE > tabla = Ayudante <>:: tabla ;     enum { CUATRO = tabla [ 2 ] // uso en tiempo de compilación };     int main () { for ( int i = 0 ; i < TABLE_SIZE ; 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 asistente de estructura hereda recursivamente de una estructura con un argumento de plantilla más (en este ejemplo calculado como ÍNDICE * ÍNDICE) 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 variables 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 ÍNDICE = 0 , int ... D > estructura Ayudante : Ayudante < ÍNDICE + 1 , D ..., ÍNDICE * ÍNDICE > { }; plantilla <> estructura Ayudante < 0 , <>> : Ayudante < 0 + 1 , 0 * 0 > { }; plantilla <> estructura Ayudante < 1 , < 0 >> : Ayudante < 1 + 1 , 0 , 1 * 1 > { }; plantilla <> estructura Ayudante < 2 , < 0 , 1 >> : Ayudante < 2 + 1 , 0 , 1 , 2 * 2 > { }; plantilla <> estructura Ayudante < 3 , < 0 , 1 , 4 >> : Ayudante < 3 + 1 , 0 , 1 , 4 , 3 * 3 > { }; plantilla <> estructura Ayudante < 4 , < 0 , 1 , 4 , 9 >> : Ayudante < 4 + 1 , 0 , 1 , 4 , 9 , 4 * 4 > { }; plantilla <> estructura 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 auxiliar < 10 , < 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 64 , 81 >> { static constexpr std :: matriz < int , TABLE_SIZE > tabla = { 0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 64 , 81 }; };                                    

Desde C++ 17, esto se puede escribir de manera más legible como:

 #incluye <iostream> #incluye <matriz>  constexpr int TABLE_SIZE = 10 ;    constexpr std :: matriz < int , TABLE_SIZE > table = [] { // O: constexpr auto table std :: matriz < int , TABLE_SIZE > A = {}; for ( sin signo i = 0 ; i < TABLE_SIZE ; i ++ ) { A [ i ] = i * i ; } devolver A ; }();                              enum { CUATRO = tabla [ 2 ] // uso en tiempo de compilación };     int main () { for ( int i = 0 ; i < TABLE_SIZE ; 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 el siguiente listado se ha ampliado para tener una ayuda 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, ...).

 #incluye <iostream> #incluye <matriz>  constexpr int TABLE_SIZE = 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 variada para una estructura auxiliar recursiva. */ plantilla < nombre de tipo VALUETYPE , VALUETYPE OFFSET , int N = 0 , VALUETYPE ... D > struct Helper : Ayudante < VALUETYPE , OFFSET , N + 1 , D ..., ValueHelper < VALUETYPE , OFFSET , N > :: valor > { };                    /** * Especialización de la plantilla para finalizar la recursividad cuando el tamaño de la tabla alcance TABLE_SIZE. */ plantilla < nombre de tipo 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 , TABLE_SIZE > tabla = Ayudante < uint16_t , OFFSET >:: tabla ;      int main () { for ( int i = 0 ; i < TABLE_SIZE ; i ++ ) { std :: cout << tabla [ i ] << std :: endl ; } }                  

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

#incluye <iostream> #incluye <matriz>  constexpr int TABLE_SIZE = 20 ; constexpr int DESPLAZAMIENTO = 12 ;        plantilla < typename VALUETYPE , int OFFSET > constexpr std :: matriz < VALUETYPE , TABLE_SIZE > table = [] { // O: constexpr auto table std :: array < VALUETYPE , TABLE_SIZE > A = {}; for ( unsigned i = 0 ; i < TABLE_SIZE ; i ++ ) { A [ i ] = OFFSET + i * i ; } devolver A ; }();                                   int main () { for ( int i = 0 ; i < TABLE_SIZE ; i ++ ) { std :: cout << tabla < uint16_t , OFFSET > [ i ] << std :: endl ; } }                   

Conceptos

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

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

A continuación se muestra un ejemplo del famoso problema del zumbido de Fizz resuelto con la metaprogramación de plantillas.

#include <boost/type_index.hpp> // para una bonita impresión de tipos #include <iostream> #include <tuple>   /** * Escriba la representación de las palabras a imprimir */ struct Fizz {}; estructura Buzz {}; estructura FizzBuzz {}; plantilla < size_t _N > número de estructura { constexpr static size_t N = _N ; };                 /** * Conceptos utilizados para definir condiciones para especializaciones */ plantilla < nombre de tipo Cualquiera > concepto has_N = requiere { requiere Cualquiera :: N - Cualquiera :: N == 0 ; }; plantilla < nombre de tipo A > concepto fizz_c = has_N < A > && requiere { requiere A :: N % 3 == 0 ; }; plantilla < nombre de tipo A > concepto buzz_c = has_N < A > && requiere { requiere A :: N % 5 == 0 ;}; plantilla < nombre de tipo A > concepto fizzbuzz_c = fizz_c < A > && buzz_c < A > ;                                              /** * Al especializar la estructura `res`, con requisitos conceptuales, se realiza la creación de instancias adecuada */ template < typename X > struct res ; plantilla < fizzbuzz_c X > estructura res < X > { usando resultado = FizzBuzz ; }; plantilla < fizz_c X > struct res < X > { usando resultado = Fizz ; }; plantilla < buzz_c X > struct res < X > { usando resultado = Buzz ; }; plantilla < has_N X > estructura res <X> { usando resultado = X ;};                                       /** * Predeclaración del concatenador */ plantilla < size_t cnt , typename ... Args > struct concatenator ;      /** * Forma recursiva de concatenar los siguientes tipos */ template < size_t cnt , typename ... Args > struct concatenator < cnt , std :: tuple < Args ... >> { usando tipo = typename concatenator < cnt - 1 , std :: tupla < nombre de tipo res < número < cnt > >:: resultado , Args ... >>:: tipo ;};                      /** * Caso base */ plantilla < nombre de tipo ... Args > concatenador de estructura < 0 , std :: tupla < Args ... >> { usando tipo = std :: tupla < Args ... > ;};          /** * Obtenedor de resultado final */ plantilla < size_t Cantidad > usando fizz_buzz_full_template = concatenador de nombre de tipo < Cantidad - 1 , std :: tupla < nombre de tipo res < número < Cantidad >>:: resultado >>:: tipo ;         int main () { // imprimiendo el resultado con boost, para que quede claro std :: cout << boost :: typeindex :: type_id < fizz_buzz_full_template < 100 >> (). nombre_bonito () << 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> */ }     

Beneficios y desventajas de la metaprogramación de plantillas

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

Ver también

Referencias

  1. ^ Scott Meyers (12 de mayo de 2005). C++ eficaz: 55 formas específicas de mejorar sus programas y diseños. Educación Pearson. ISBN 978-0-13-270206-5.
  2. ^ Ver Historia de TMP en Wikilibros
  3. ^ Veldhuizen, Todd L. (2003). "Las plantillas de C++ están completas en Turing". CiteSeerX 10.1.1.14.3670 . 
  4. ^ "Constexpr - Expresiones constantes generalizadas en C++ 11 - Cprogramming.com". www.cprogramming.com .
  5. ^ "Fachada iteradora - 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, plantilla 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 limitaciones del compilador (aunque esto ha mejorado significativamente en los últimos años), falta de soporte de depuración o IO durante la creación de instancias de la plantilla, tiempos de compilación prolongados, errores largos e incomprensibles, mala legibilidad del código y notificación deficiente de errores.
  8. ^ Cizalla, Tim; Jones, Simon Peyton (2002). "Plantilla de metaprogramación para 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 maneras fascinantes que van más allá de los sueños más locos de los diseñadores de lenguajes. Quizás 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