Introducción a la metaprogramación en C ++

Anterior 1 2 3 Página 3 Página 3 de 3
  • Variables de estado: los parámetros de la plantilla
  • Construcciones de bucle: mediante recursividad
  • Elección de rutas de ejecución: mediante el uso de expresiones condicionales o especializaciones
  • Aritmética de enteros

Si no hay límites para la cantidad de instanciaciones recursivas y la cantidad de variables de estado permitidas, esto es suficiente para calcular cualquier cosa que sea computable. Sin embargo, puede que no sea conveniente hacerlo mediante plantillas. Además, debido a que la instanciación de plantillas requiere recursos sustanciales del compilador, la instanciación recursiva extensa ralentiza rápidamente un compilador o incluso agota los recursos disponibles. El estándar C ++ recomienda, pero no exige, que se permitan como mínimo 1.024 niveles de instanciaciones recursivas, lo cual es suficiente para la mayoría (pero ciertamente no para todas) las tareas de metaprogramación de plantillas.

Por tanto, en la práctica, los metaprogramas de plantilla deben utilizarse con moderación. Sin embargo, hay algunas situaciones en las que son insustituibles como herramienta para implementar plantillas convenientes. En particular, a veces se pueden ocultar en las entrañas de las plantillas más convencionales para obtener un mayor rendimiento de las implementaciones de algoritmos críticos.

Instanciación recursiva versus argumentos de plantilla recursivos

Considere la siguiente plantilla recursiva:

plantilla struct Doublify {}; template struct Trouble {usando LongType = Doublify
   
    ; }; plantilla struct Problema {usando LongType = double; }; Problema :: LongType ouch;
   

El uso de Trouble::LongTypeno sólo desencadena la creación de instancias recursivas de Trouble, Trouble, ..., Trouble, sino que también crea una instancia Doublifysobre tipos cada vez más complejos. La tabla ilustra lo rápido que crece.

El crecimiento de Trouble::LongType

 
Tipo de alias Tipo subyacente
Trouble::LongType double
Trouble::LongType Doublify
Trouble::LongType Doublify

   Doublify>

Trouble::LongType Doublify

     Doublify>,

  

     Doublify>>

Como muestra la tabla, la complejidad de la descripción del tipo de la expresión Trouble::LongTypecrece exponencialmente con N. En general, tal situación estresa un compilador de C ++ incluso más que las instancias recursivas que no involucran argumentos de plantilla recursivos. Uno de los problemas aquí es que un compilador mantiene una representación del nombre mutilado para el tipo. Este nombre mutilado codifica la especialización de plantilla exacta de alguna manera, y las primeras implementaciones de C ++ usaban una codificación que es aproximadamente proporcional a la longitud de la identificación de la plantilla. Estos compiladores luego usaron más de 10,000 caracteres para Trouble::LongType.

Las implementaciones más recientes de C ++ tienen en cuenta el hecho de que los ID de plantilla anidados son bastante comunes en los programas modernos de C ++ y utilizan técnicas de compresión inteligentes para reducir considerablemente el crecimiento en la codificación de nombres (por ejemplo, algunos cientos de caracteres para Trouble::LongType). Estos compiladores más nuevos también evitan generar un nombre mutilado si no se necesita ninguno porque en realidad no se genera código de bajo nivel para la instancia de plantilla. Aún así, en igualdad de condiciones, probablemente sea preferible organizar la instanciación recursiva de tal forma que los argumentos de plantilla no necesiten anidarse también de forma recursiva.

Valores de enumeración frente a constantes estáticas

En los primeros días de C ++, los valores de enumeración eran el único mecanismo para crear "constantes verdaderas" (llamadas expresiones-constantes ) como miembros con nombre en declaraciones de clase. Con ellos, podría, por ejemplo, definir un Pow3metaprograma para calcular potencias de 3 de la siguiente manera:

meta / pow3enum.hpp // plantilla principal para calcular 3 en la plantilla Nth struct Pow3 {enum {value = 3 * Pow3 :: value}; }; // especialización completa para finalizar la plantilla de recursividad struct Pow3 {enum {value = 1}; };

La estandarización de C ++ 98 introdujo el concepto de inicializadores constantes estáticos en clase, de modo que el metaprograma Pow3 podría verse de la siguiente manera:

meta / pow3const.hpp // plantilla principal para calcular 3 a la plantilla Nth struct Pow3 {static int const value = 3 * Pow3 :: value; }; // especialización completa para finalizar la plantilla de recursividad struct Pow3 {static int const value = 1; };

Sin embargo, hay un inconveniente con esta versión: los miembros constantes estáticos son valores l. Entonces, si tiene una declaración como

void foo (int const &);

y le pasas el resultado de un metaprograma:

foo (Pow3 :: valor);

un compilador debe pasar la dirección de Pow3::value, y eso obliga al compilador a crear una instancia y asignar la definición para el miembro estático. Como resultado, el cálculo ya no se limita a un efecto puro de "tiempo de compilación".

Los valores de enumeración no son lvalores (es decir, no tienen una dirección). Entonces, cuando los pasa por referencia, no se usa memoria estática. Es casi exactamente como si pasara el valor calculado como literal.

C ++ 11, sin embargo, introdujo constexprmiembros de datos estáticos, que no se limitan a tipos integrales. No resuelven el problema de la dirección planteado anteriormente, pero a pesar de esa deficiencia, ahora son una forma común de producir resultados de metaprogramas. Tienen la ventaja de tener un tipo correcto (a diferencia de un tipo de enumeración artificial), y ese tipo se puede deducir cuando el miembro estático se declara con el especificador de tipo automático. C ++ 17 agregó miembros de datos estáticos en línea, que resuelven el problema de direcciones mencionado anteriormente y se pueden usar con constexpr.

Historia de la metaprogramación

El primer ejemplo documentado de un metaprograma fue el de Erwin Unruh, que entonces representaba a Siemens en el comité de estandarización de C ++. Observó la integridad computacional del proceso de creación de instancias de plantillas y demostró su punto al desarrollar el primer metaprograma. Usó el compilador de Metaware y lo convenció para que emitiera mensajes de error que contenían números primos sucesivos. Aquí está el código que se circuló en una reunión del comité de C ++ en 1994 (modificado para que ahora se compile en compiladores conformes al estándar):

plantilla meta / unruh.cpp // cálculo de números primos // (modificado con permiso del original de 1994 por Erwin Unruh)
   
     struct is_prime {enum ((p% i) && is_prime2? p: 0), i-1> :: pri); }; template struct is_prime {enum {pri = 1}; }; template struct is_prime {enum {pri = 1}; }; modelo
    
      struct D { D(void*); }; template
     
       struct CondNull { static int const value = i; }; template struct CondNull { static void* value; }; void* CondNull::value = 0; template
      
        struct Prime_print {
       

// primary template for loop to print prime numbers Prime_print a; enum { pri = is_prime::pri }; void f() { D d = CondNull::value;

// 1 is an error, 0 is fine a.f(); } }; template struct Prime_print {

// full specialization to end the loop enum {pri=0}; void f() { D d = 0; }; }; #ifndef LAST #define LAST 18 #endif int main() { Prime_print a; a.f(); }

If you compile this program, the compiler will print error messages when, in Prime_print::f(), the initialization of d fails. This happens when the initial value is 1 because there is only a constructor for void*, and only 0 has a valid conversion to void*. For example, on one compiler, we get (among several other messages) the following errors:

unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’ unruh.cpp:39:14: error: no viable conversion from ’const int’ to ’D’

Note: As error handling in compilers differs, some compilers might stop after printing the first error message.

The concept of C++ template metaprogramming as a serious programming tool was first made popular (and somewhat formalized) by Todd Veldhuizen in his paper “Using C++ Template Metaprograms.” Veldhuizen’s work on Blitz++ (a numeric array library for C++) also introduced many refinements and extensions to metaprogramming (and to expression template techniques).

Both the first edition of this book and Andrei Alexandrescu’s Modern C++ Design contributed to an explosion of C++ libraries exploiting template-based metaprogramming by cataloging some of the basic techniques that are still in use today. The Boost project was instrumental in bringing order to this explosion. Early on, it introduced the MPL (metaprogramming library), which defined a consistent framework for type metaprogramming made popular also through David Abrahams and Aleksey Gurtovoy’s book C++ Template Metaprogramming.

Additional important advances have been made by Louis Dionne in making metaprogramming syntactically more accessible, particularly through his Boost.Hana library. Dionne, along with Andrew Sutton, Herb Sutter, David Vandevoorde, and others are now spearheading efforts in the standardization committee to give metaprogramming first-class support in the language. An important basis for that work is the exploration of what program properties should be available through reflection; Matúš Chochlík, Axel Naumann, and David Sankel are principal contributors in that area.

John J. Barton and Lee R. Nackman illustrated how to keep track of dimensional units when performing computations. The SIunits library was a more comprehensive library for dealing with physical units developed by Walter Brown. The std::chrono component in the standard library only deals with time and dates, and was contributed by Howard Hinnant.