template - plantillas c++
¿Alguien usa metaprogramación de plantillas en la vida real? (15)
Descubrí la metaprogramación de plantillas hace más de 5 años y me entusiasmé enormemente con la lectura de Modern C ++ Design, pero nunca encontré la oportunidad de usarlo en la vida real.
¿ Alguna vez ha usado esta técnica en código real?
Los colaboradores de Boost no necesitan postularse; o)
Casi 8 meses después de preguntar esto, finalmente utilicé un TMP, utilicé un TypeList de interfaces para implementar QueryInterface en una clase base.
Encontré las políticas, que se describen en Modern C ++ Design, realmente útiles en dos situaciones:
Cuando estoy desarrollando un componente que espero será reutilizado, pero de una manera ligeramente diferente. La sugerencia de Alexandrescu de usar una política para reflejar un diseño encaja muy bien aquí: me ayuda a hacer preguntas pasadas como: "Podría hacer esto con un hilo de fondo, pero ¿y si alguien más tarde quiere hacerlo en rodajas de tiempo?" De acuerdo, escribo mi clase para aceptar una ConcurrencyPolicy e implementar la que necesito en este momento. Entonces, al menos, sé que la persona que viene detrás de mí puede escribir y agregar una nueva política cuando la necesiten, sin tener que volver a trabajar totalmente en mi diseño. Advertencia: tengo que reinar a veces o esto puede perder el control. ¡Recuerda el principio de YAGNI !
Cuando estoy tratando de refactorizar varios bloques de código similares en uno. Por lo general, el código se copiará y se modificará ligeramente porque de lo contrario habría tenido demasiada lógica if / else, o porque los tipos involucrados eran demasiado diferentes. Descubrí que las políticas a menudo permiten una versión limpia para todos, donde la lógica tradicional o la herencia múltiple no lo hacen.
La mayoría de los programadores que usan metaprogramación de plantillas lo usan indirectamente, a través de bibliotecas como boost. Probablemente ni siquiera saben lo que sucede entre bastidores, solo que hace que la sintaxis de ciertas operaciones sea mucho más fácil.
La metaprogramación de plantillas es una técnica maravillosa y poderosa cuando se escriben bibliotecas c ++. Lo he usado algunas veces en soluciones personalizadas, pero generalmente una solución c ++ de estilo antiguo menos elegante es más fácil de obtener a través de la revisión del código y más fácil de mantener para otros usuarios.
Sin embargo, saqué mucho provecho de la meta programación de plantillas al escribir componentes / bibliotecas reutilizables. No estoy hablando de algo tan grande como algunas de las cosas de Boost, solo componentes pequeños que se reutilizarán con frecuencia.
Utilicé TMP para un sistema singleton donde el usuario podía especificar qué tipo de singleton deseaban. La interfaz fue muy básica. Debajo estaba alimentado por TMP pesado.
template< typename T >
T& singleton();
template< typename T >
T& zombie_singleton();
template< typename T >
T& phoenix_singleton();
Otro uso exitoso fue la simplificación de nuestra capa de IPC. Está construido con el estilo clásico de OO. Cada mensaje debe derivarse de una clase base abstracta y anular algunos métodos de serialización. Nada demasiado extremo, pero genera mucho código de placa de caldera.
Lanzamos algo de TMP y automatizamos la generación de todo el código para el caso simple de mensajes que contienen solo datos POD. Los mensajes TMP todavía utilizan el back-end OO pero reducen masivamente la cantidad de código de la placa de la caldera. El TMP también se usó para generar el mensaje vistor. Con el tiempo, todos nuestros mensajes migraron al método TMP. Era más fácil y menos código construir una estructura POD simple solo para pasar mensajes y agregar las pocas (quizás 3) líneas necesarias para que el TMP generara las clases de lo que era derivar un nuevo mensaje para enviar una clase regular a través del IPC marco de referencia.
Las plantillas de metaprogramación y expresión de plantillas se están volviendo más populares en la comunidad científica como métodos de optimización que descargan parte del esfuerzo computacional en el compilador mientras mantienen cierta abstracción. El código resultante es más grande y menos legible, pero he usado estas técnicas para acelerar las bibliotecas de álgebra lineal y los métodos de cuadratura en las bibliotecas FEM.
Para la lectura específica de la aplicación, Todd Veldhuizen es un gran nombre en esta área. Un libro popular es C ++ y Computación Numérica Orientado a Objetos para Científicos e Ingenieros por Daoqi Yang.
Lo he usado bastante con el código DSP, especialmente las FFT, los búferes circulares de tamaño fijo, las transformaciones de Hadamard y similares.
Lo he usado en los bucles internos del código de gráficos de un juego, donde desea un cierto nivel de abstracción y modularidad, pero no puede pagar el costo de las sucursales o las llamadas virtuales. En general, fue una mejor solución que una proliferación de funciones manuscritas para casos especiales.
Lo uso con boost :: statechart para grandes máquinas de estado.
Muchos programadores no usan plantillas mucho debido a la poca compatibilidad del compilador hasta hace poco. Sin embargo, aunque las plantillas han tenido muchos problemas en el pasado, los compiladores más nuevos tienen un soporte mucho mejor. Escribo código que tiene que funcionar con GCC en Mac y Linux, así como en Microsoft Visual C ++, y es solo con GCC 4 y VC ++ 2005 que estos compiladores han soportado muy bien el estándar.
La programación genérica a través de plantillas no es algo que necesite todo el tiempo, pero definitivamente es un código útil para tener en su caja de herramientas.
El ejemplo obvio de clases de contenedor pero plantillas también son útiles para muchas otras cosas. Dos ejemplos de mi propio trabajo son:
- Punteros inteligentes (por ejemplo, contados por referencia, copia por escritura, etc.)
- Clases de soporte matemático tales como Matrices, vectores, splines, etc. que necesitan soportar una variedad de tipos de datos y aún así ser eficientes.
No hagas eso. La razón detrás de esto es la siguiente: por naturaleza de la metaprogramación de plantillas, si alguna parte de su lógica se realiza en tiempo de compilación, cada lógica de la que depende debe hacerse también en tiempo de compilación. Una vez que lo inicie, haga una parte de su lógica en tiempo de compilación, no hay devolución. La bola de nieve seguirá rodando y no hay forma de detenerla.
Por ejemplo, no puede iterar en los elementos de un boost :: tuple <>, porque solo puede acceder a ellos en tiempo de compilación. Debe usar la metaprogramación de plantillas para lograr lo que hubiera sido sencillo y directo C ++, y esto siempre sucede cuando los usuarios de C ++ no son lo suficientemente cuidadosos para no mover demasiadas cosas al tiempo de compilación. A veces es difícil ver cuándo un cierto uso de la lógica de compilación se volvería problemático, y a veces los programadores están ansiosos por probar y probar lo que han leído en Alexandrescu. En cualquier caso, esta es una muy mala idea en mi opinión.
No, no lo he usado en el código de producción.
¿Por qué?
- Tenemos que admitir más de 6 plataformas con compiladores nativos de plataformas . Ya es bastante difícil usar STL en este entorno, y mucho menos técnicas de plantilla modernas.
- Los desarrolladores ya no parecen seguir avanzando en C ++. Usamos C ++ cuando tenemos que hacerlo. Tenemos código heredado con diseños heredados. El nuevo código se realiza en otra cosa, por ejemplo, Java, Javascript, Flash.
Para quienes estén familiarizados con Oracle Template Library ( OTL ), boost :: any y la biblioteca Loki (la que se describe en Modern C ++ Design), aquí está el código TMP de prueba de concepto que le permite almacenar una fila de otl_stream en el vector<boost::any>
contenedor y datos de acceso por número de columna. Y ''Sí'', voy a incorporarlo en el código de producción.
#include <iostream>
#include <vector>
#include <string>
#include <Loki/Typelist.h>
#include <Loki/TypeTraits.h>
#include <Loki/TypeManip.h>
#include <boost/any.hpp>
#define OTL_ORA10G_R2
#define OTL_ORA_UTF8
#include <otlv4.h>
using namespace Loki;
/* Auxiliary structs */
template <int T1, int T2>
struct IsIntTemplateEqualsTo{
static const int value = ( T1 == T2 );
};
template <int T1>
struct ZeroIntTemplateWorkaround{
static const int value = ( 0 == T1? 1 : T1 );
};
/* Wrapper class for data row */
template <class TList>
class T_DataRow;
template <>
class T_DataRow<NullType>{
protected:
std::vector<boost::any> _data;
public:
void Populate( otl_stream& ){};
};
/* Note the inheritance trick that enables to traverse Typelist */
template <class T, class U>
class T_DataRow< Typelist<T, U> >:public T_DataRow<U>{
public:
void Populate( otl_stream& aInputStream ){
T value;
aInputStream >> value;
boost::any anyValue = value;
_data.push_back( anyValue );
T_DataRow<U>::Populate( aInputStream );
}
template <int TIdx>
/* return type */
Select<
IsIntTemplateEqualsTo<TIdx, 0>::value,
typename T,
typename TL::TypeAt<
U,
ZeroIntTemplateWorkaround<TIdx>::value - 1
>::Result
>::Result
/* sig */
GetValue(){
/* body */
return boost::any_cast<
Select<
IsIntTemplateEqualsTo<TIdx, 0>::value,
typename T,
typename TL::TypeAt<
U,
ZeroIntTemplateWorkaround<TIdx>::value - 1
>::Result
>::Result
>( _data[ TIdx ] );
}
};
int main(int argc, char* argv[])
{
db.rlogon( "AMONRAWMS/[email protected]" ); // connect to Oracle
std::cout<<"Connected to oracle DB"<<std::endl;
otl_stream o( 1, "select * from blockstatuslist", db );
T_DataRow< TYPELIST_3( int, int, std::string )> c;
c.Populate( o );
typedef enum{ rcnum, id, name } e_fields;
/* After declaring enum you can actually acess columns by name */
std::cout << c.GetValue<rcnum>() << std::endl;
std::cout << c.GetValue<id>() << std::endl;
std::cout << c.GetValue<name>() << std::endl;
return 0;
};
Para aquellos que no están familiarizados con las bibliotecas mencionadas.
El problema con el contenedor otl_stream de OTL es que uno puede acceder a los datos de las columnas solo en orden secuencial declarando las variables del tipo apropiado y aplicando el operator >>
al objeto otl_stream de la siguiente manera:
otl_stream o( 1, "select * from blockstatuslist", db );
int rcnum;
int id;
std::string name;
o >> rcnum >> id >> name;
No siempre es conveniente. La solución consiste en escribir una clase contenedora y completarla con datos de otl_stream. El deseo es poder declarar la lista de tipos de columnas y luego:
- tomar el tipo T de la columna
- declarar variable de ese tipo
- aplicar
olt_stream::operator >>(T&)
- almacena el resultado (en el vector de boost :: any)
- tomar el tipo de la columna siguiente y repetir hasta que se procesen todas las columnas
Puede hacer todo esto con la ayuda de la estructura de Typelist
de Typelist
de Loki, la especialización de plantillas y la herencia.
Con la ayuda de las construcciones de la biblioteca de Loki, también puede generar un montón de funciones de GetValue que devuelven valores del tipo apropiado deduciéndolo del número de columna (en realidad, número de tipo en la Typelist
de Typelist
).
Sí, lo tengo, sobre todo para hacer algunas cosas que se asemejan a pato-tipado cuando estaba envolviendo una API heredada en una interfaz C ++ más moderna.
Una vez utilicé la metaprogramación de plantillas en C ++ para implementar una técnica llamada "perturbación simbólica" para tratar la entrada degenerada en algoritmos geométricos. Al representar expresiones aritméticas como plantillas anidadas (es decir, básicamente escribiendo a mano los árboles de análisis sintáctico) pude transferir todo el análisis de expresiones al procesador de plantillas.
Hacer este tipo de cosas con plantillas es más eficiente que, por ejemplo, escribir árboles de expresiones usando objetos y hacer el análisis en tiempo de ejecución. Es más rápido porque el árbol de expresiones modificado (perturbado) está disponible para el optimizador al mismo nivel que el resto del código, por lo que obtienes todos los beneficios de la optimización, tanto dentro de tus expresiones como (siempre que sea posible) entre tus expresiones y el código circundante.
Por supuesto, podría lograr lo mismo al implementar un DSL pequeño (lenguaje específico del dominio) para sus expresiones y pegar el código C ++ traducido en su programa habitual. Eso le daría todos los mismos beneficios de optimización y también sería más legible, pero la compensación es que debe mantener un analizador.
Utilizo la metaprogramación de plantillas todo el tiempo, pero en D, no en C ++. El metalenguaje de plantillas de C ++ se diseñó originalmente para la parametrización de tipo simple y se convirtió en un metalenguaje completo de Turing casi por accidente. Por lo tanto, es un tarpit de Turing que solo Andrei Alexandrescu, no simples mortales, puede usar.
El lenguaje de la plantilla de D, por otro lado, en realidad fue diseñado para la metaprogramación más allá de la parametrización de tipo simple. A Andrei Alexandrescu parece encantarlo, pero otras personas pueden entender sus plantillas D. También es lo suficientemente potente como para que alguien haya escrito un raytracer en tiempo de compilación como prueba de concepto.
Supongo que el metaprograma más útil / no trivial que escribí en D fue una plantilla de función que, dado un tipo de estructura como parámetro de plantilla y una lista de nombres de encabezado de columna en un orden correspondiente a las declaraciones de variables en la estructura como tiempo de ejecución parámetro, leerá en un archivo CSV y devolverá una matriz de estructuras, una para cada fila, con cada campo de estructura correspondiente a una columna. Todas las conversiones de tipo (cadena a flotante, int, etc.) se realizan automáticamente, en función de los tipos de campos de la plantilla.
Otro bueno, que funciona principalmente, pero aún no maneja unos pocos casos correctamente, es una plantilla de función de copia profunda que maneja estructuras, clases y matrices correctamente. Utiliza solo reflexión / introspección en tiempo de compilación para que pueda funcionar con estructuras que, a diferencia de las clases completas, no tienen capacidades de reflexión / introspección de tiempo de ejecución en D porque se supone que son livianas.