2009-08-03 16 views
30

tengo un problema .. no entiendo Plantilla MetaprogramaciónPlantilla Metaprogramación - Todavía no lo entiendo :(

El problema es: He leído mucho pero no tiene mucho sentido.. me:/

Nr.1 ​​Hecho: Plantilla Metaprogramación es más rápido

template <int N> 
struct Factorial 
{ 
    enum { value = N * Factorial<N - 1>::value }; 
}; 

template <> 
struct Factorial<0> 
{ 
    enum { value = 1 }; 
}; 

// Factorial<4>::value == 24 
// Factorial<0>::value == 1 
void foo() 
{ 
    int x = Factorial<4>::value; // == 24 
    int y = Factorial<0>::value; // == 1 
} 

por lo que este metaprograma es más rápido ... pd de la constante literal
. PERO: ¿Dónde en el mundo real tenemos constantes literales?
La mayoría de los programas que uso reaccionan a la entrada del usuario.

FACT nr. 2: la metaprogramación de la plantilla puede realizar mejor capacidad de mantenimiento.

Sí. El ejemplo factorial puede ser mantenible ... pero cuando se trata de funciones complejas, yo y la mayoría de los otros programadores de C++ no puedo leer las funciones. Y las opciones de depuración son pobres. (O al menos no sé cómo depurar)

¿Dónde tiene sentido la metaprogramación de plantillas?

+3

Nunca tuvo mucho sentido para mí ... – Kawa

+10

No te sientas mal, casi nadie lo entiende. –

Respuesta

26

Así como factorial no es un ejemplo realista de recursión en lenguajes no funcionales, tampoco es un ejemplo realista de metaprogramación de plantillas. Es solo el ejemplo estándar al que recurren las personas cuando quieren mostrarle recursividad.

Al escribir plantillas para fines realistas, como en las bibliotecas cotidianas, a menudo la plantilla tiene que adaptar lo que hace dependiendo de los parámetros de tipo con los que se crea una instancia. Esto puede volverse bastante complejo, ya que la plantilla elige efectivamente qué código generar, condicionalmente. Esto es lo que es la metaprogramación de la plantilla; si la plantilla tiene que repetirse (mediante recursividad) y elegir entre alternativas, es efectivamente como un pequeño programa que se ejecuta durante la compilación para generar el código correcto.

Aquí hay un buen tutorial de las páginas de documentación de refuerzo (en realidad, extraído de brilliant book, vale la pena leerlo).

http://www.boost.org/doc/libs/1_39_0/libs/mpl/doc/tutorial/representing-dimensions.html

+3

Buena respuesta. +1 - Desearía que la gente hubiera acordado un mejor ejemplo estándar. En general, usar metaprogramación para calcular valores no tiene sentido. Es esclarecedor como un ejercicio de aprendizaje, al tratar de entender TMP, pero no como un caso de uso, y no como un argumento de venta para convencer a la gente de cuán asombrosa es la función. – jalf

+1

No descartaría completamente la generación de valor :) http://stackoverflow.com/questions/699781/c-binary-constant-literal –

+0

Por supuesto que puede ser útil, pero no es realmente el gran argumento para convencer a las personas que la característica vale la pena El ejemplo binario es mejor que de costumbre, pero aún así, la pregunta obvia es "¿por qué no solo hacerlo en tiempo de ejecución? No es prohibitivo". ¿Es eso realmente una justificación para acoplar otro lenguaje de turing completo sobre C++? ;) – jalf

2

Sugiero que lea Modern C++ Design por Andrei Alexandrescu - este es probablemente uno de los mejores libros sobre usos reales de la metaprogramación de plantillas C++; y describe muchos problemas que las plantillas de C++ son una excelente solución.

+14

Todos los desarrolladores de C++ deben leer ese libro, pero no es tan útil para responder la pregunta, por la misma razón que simplemente proporcionar un enlace a Google suele ser desaprobado. Se supone que SO es un lugar para encontrar respuestas, no enlaces a otros recursos donde puede intentar buscar en su lugar. – jalf

+0

La tercera edición de Cype Effective C++ de Scott Meyers también contiene un ejemplo que se explica completamente. (Aunque se trata de sobrecarga de funciones en lugar de funciones meta y, por lo tanto, podría no parecer "TMP real" al principio). – sbi

2

TMP se puede utilizar desde algo como asegurar exactitud dimensional (Asegurarse de que la masa no puede ser dividida por el tiempo, pero la distancia se puede dividir por el tiempo y asigna a una variable de velocidad) para optimizar las operaciones de la matriz mediante la eliminación temporal objetos y bucles de fusión cuando están involucradas muchas matrices.

+0

La masa se puede dividir por tiempo. El resultado solo tiene que ser del tipo "masa por tiempo";) Pero sí, TMP se puede usar para hacer cumplir eso. – jalf

+0

Cualquier cosa puede ser dividida/multiplicada por cualquier cosa, es la suma/resta lo que hace tropezar a la gente. –

3

Scott Meyers ha estado trabajando para imponer restricciones de código utilizando TMP.

Su absolutamente una buena lectura:
http://www.artima.com/cppsource/codefeatures.html

En este artículo se presentan los conceptos de conjuntos de tipos (no es un concepto nuevo, pero su trabajo se basa sábana de este concepto). Luego utiliza TMP para asegurarse de que no importa en qué orden especifique los miembros del conjunto, si dos conjuntos están formados por los mismos miembros, entonces son equivalentes. Esto requiere que sea capaz de ordenar y reordenar una lista de tipos y compararlos dinámicamente generando errores de tiempo de compilación cuando no coinciden.

+0

En realidad su nombre es "Meyers". (Lo siento por curiosear.) – sbi

+0

Corregido. :-) –

9

por lo que este metaprograma es más rápido ... por el constante literal. PERO: ¿Dónde en el mundo real tenemos Literales constantes? La mayoría de los programas que uso reaccionan a la entrada del usuario.

Es por eso que casi nunca se usa para valores. Por lo general, se usa en tipos. usar tipos para calcular y generar nuevos tipos.

hay muchos en el mundo real utiliza, algunas de las cuales ya está familiarizado con, incluso si no se dan cuenta.

Uno de mis ejemplos favoritos es el de iteradores. En su mayoría están diseñados solo con programación genérica, sí, pero la metaprogramación de plantillas es útil en un lugar en particular:

Para arreglar punteros para que puedan usarse como iteradores. Un iterador debe exponer un puñado de typedef, como value_type. Los punteros no hacen eso.

Así código como el siguiente (básicamente idéntica a lo que se encuentra en Boost.Iterator)

template <typename T> 
struct value_type { 
    typedef typename T::value_type type; 
}; 

template <typename T> 
struct value_type<T*> { 
    typedef T type; 
}; 

es una muy simple metaprograma plantilla, pero que es muy útil. Le permite obtener el tipo de valor de cualquier tipo de iterador T, ya sea un puntero o una clase, simplemente por value_type<T>::type.

Y creo que lo anterior tiene algunas ventajas muy claras cuando se trata de mantenimiento. Su algoritmo que opera en iteradores solo tiene que implementarse una vez. Sin este truco, tendrías que hacer una implementación para punteros y otra para iteradores "propios" basados ​​en clases.

Trucos como boost::enable_if pueden ser muy valiosos también. Tiene una sobrecarga de una función que solo debería habilitarse para un conjunto específico de tipos. En lugar de definir una sobrecarga para cada tipo, puede usar la metaprogramación para especificar la condición y pasarla al enable_if.

Earwicker ya se ha mencionado otro buen ejemplo, un marco para la expresión de unidades físicas y dimensiones. Le permite expresar cálculos como con unidades físicas adjuntas, y aplica el tipo de resultado. La multiplicación de metros por metros produce una cantidad de metros cuadrados. La metaprogramación de plantillas se puede usar para producir automáticamente el tipo correcto.

Pero la mayoría de las veces, la metaprogramación de plantillas se usa (y es útil) en casos pequeños y aislados, básicamente para suavizar protuberancias y casos excepcionales, para que un conjunto de tipos se vea y se comporte de manera uniforme, lo que le permite usar programación genérica de manera más eficiente

8

adscripción de la recomendación para Alexandrescu moderno C++ Diseño.

Las plantillas realmente brillan cuando estás escribiendo una biblioteca que tiene piezas que se pueden ensamblar combinatoriamente en un enfoque "elige un Foo, un Bar y un Baz", y esperas que los usuarios hagan uso de estas piezas de alguna forma eso es fijo en tiempo de compilación. Por ejemplo, fui coautor de una biblioteca de minería de datos que usa metaprogramación de plantillas para permitir que el programador decida qué va a usar DecisionType (clasificación, clasificación o regresión), qué InputType esperar (flotantes, ints, valores enumerados, lo que sea) y qué KernelMethod usar (es una cosa de minería de datos). Luego implementamos varias clases diferentes para cada categoría, de modo que hubo varias docenas de combinaciones posibles.

La implementación de 60 clases separadas para hacer esto habría implicado una gran cantidad de duplicación de código molesto y difícil de mantener. La metaprogramación de plantillas significaba que podíamos implementar cada concepto como una unidad de código, y darle al programador una interfaz simple para crear instancias de combinaciones de estos conceptos en tiempo de compilación.

El análisis dimensional también es un excelente ejemplo, pero otras personas lo han cubierto.

También escribí algunos simples generadores de números pseudoaleatorios en tiempo de compilación solo para meterse con las cabezas de las personas, pero eso realmente no cuenta con IMO.

+5

¡Jugar con las cabezas de las personas es uno de los mejores usos para los que se puede programar! No subestimes el valor de eso. :pag – jalf

15

Utilizo plantillas de programación de meteo para los operadores SSE swizzling para optimizar las mezclas durante el tiempo de compilación.

swizzles SSE ('baraja') sólo pueden ser enmascarados como un byte literal (valor inmediato), así que creamos una clase de plantilla 'máscara de fusión' que combina máscaras durante el tiempo de compilación para cuando se producen múltiples aleatoria:

template <unsigned target, unsigned mask> 
struct _mask_merger 
{ 
    enum 
    { 
     ROW0 = ((target >> (((mask >> 0) & 3) << 1)) & 3) << 0, 
     ROW1 = ((target >> (((mask >> 2) & 3) << 1)) & 3) << 2, 
     ROW2 = ((target >> (((mask >> 4) & 3) << 1)) & 3) << 4, 
     ROW3 = ((target >> (((mask >> 6) & 3) << 1)) & 3) << 6, 

     MASK = ROW0 | ROW1 | ROW2 | ROW3, 
    }; 
}; 

Esto funciona y produce código notable sin sobrecarga de código generada y poco tiempo de compilación adicional.

3

Aquí hay un ejemplo trivial, un convertidor constante binaria, a partir de una pregunta anterior aquí en StackOverflow:

C++ binary constant/literal

template< unsigned long long N > 
struct binary 
{ 
    enum { value = (N % 10) + 2 * binary<N/10> :: value } ; 
}; 
template<> 
struct binary<0> 
{ 
    enum { value = 0 } ; 
}; 
5

El ejemplo factorial es lo más útil para el mundo real TMP como "Hola, ¡mundo!" es para programación común: está ahí para mostrarle algunas técnicas útiles (recursión en lugar de iteración, "else-if-then", etc.) en un ejemplo muy simple y relativamente fácil de entender que no tiene mucha relevancia para cada día de codificación (¿Cuándo fue la última vez que tenía que escribir un programa que emite "Hola, mundo"?)

TMP se trata de la ejecución de algoritmos en tiempo de compilación y esto implica algunas ventajas obvias:

  • Dado que estos la falla de los algoritmos significa que su código no se compila, los algoritmos que fallan nunca llegan a su cliente y, por lo tanto, no pueden fallar en el cliente. Para mí, durante la última década, esta fue la ventaja más importante que me llevó a introducir TMP en el código de las empresas para las que trabajé.
  • Dado que el resultado de ejecutar plantillas-metaprogramas es un código ordinario que luego compila el compilador, se aplican todas las ventajas de los algoritmos de generación de código (redundancia reducida, etc.).
  • Por supuesto, dado que se ejecutan en tiempo de compilación, estos algoritmos no necesitarán ningún tiempo de ejecución y, por lo tanto, se ejecutarán más rápido.TMP se trata principalmente de computación en tiempo de compilación con algunas funciones en línea, en su mayoría pequeñas, entrecruzadas, por lo que los compiladores tienen amplias oportunidades para optimizar el código resultante.

Por supuesto, hay desventajas también:

  • Los mensajes de error pueden ser horrible.
  • No hay depuración.
  • El código a menudo es difícil de leer.

Como siempre, solo tendrá que ponderar las ventajas frente a las desventajas en todos los casos.

En cuanto a una mayor utilidad ejemplo: Una vez que haya captado listas de tipos y algoritmos básicos de tiempo de compilación que operan en ellos, es posible entender lo siguiente:

typedef 
    type_list_generator< signed char 
         , signed short 
         , signed int 
         , signed long 
         >::result_type 
    signed_int_type_list; 

typedef 
    type_list_find_if< signed_int_type_list 
        , exact_size_predicate<8> 
        >::result_type 
    int8_t; 

typedef 
    type_list_find_if< signed_int_type_list 
        , exact_size_predicate<16> 
        >::result_type 
    int16_t; 

typedef 
    type_list_find_if< signed_int_type_list 
        , exact_size_predicate<32> 
        >::result_type 
    int32_t; 

Esta es (ligeramente simplificado) código real Lo escribí hace unas semanas. Escogerá los tipos apropiados de una lista de tipos, reemplazando las orgías #ifdef comunes en el código portátil. No necesita mantenimiento, funciona sin adaptación en cada plataforma en la que su código podría necesitar ser portado, y emite un error de compilación si la plataforma actual no tiene el tipo correcto.

Otro ejemplo es la siguiente:

template< typename TFunc, typename TFwdIter > 
typename func_traits<TFunc>::result_t callFunc(TFunc f, TFwdIter begin, TFwdIter end); 

Dada una función f y una secuencia de cadenas, esto va a diseccionar la firma de la función, convertir las cadenas de la secuencia en el tipo correcto, y llamar a la función con estos objetos. Y es principalmente TMP adentro.

3

TMP no significa necesariamente un código más rápido o más fácil de mantener. Usé la biblioteca boost spirit para implementar un analizador simple de expresiones SQL que construye una estructura de árbol de evaluación. Si bien el tiempo de desarrollo se redujo ya que tenía cierta familiaridad con TMP y lambda, la curva de aprendizaje es un muro de ladrillos para los desarrolladores de "C con clases", y el rendimiento no es tan bueno como un LEX/YACC tradicional.

Veo la metaprogramación de plantillas como una herramienta más en mi herramienta-cinturón. Cuando funcione para usarlo, si no lo hace, use otra herramienta.

2

'static const' values ​​funcionan también. Y punteros a miembro. ¡Y no olvide el mundo de los tipos (explícitos y deducidos) como argumentos en tiempo de compilación!

PERO: ¿Dónde en el mundo real tenemos constante Literals?

Supongamos que tiene algún código que debe ejecutarse lo más rápido posible. En realidad, contiene el bucle interno crítico de su cálculo vinculado a la CPU. Estarías dispuesto a aumentar un poco el tamaño de tu ejecutable para hacerlo más rápido. Parece que:

double innerLoop(const bool b, const vector<double> & v) 
{ 
    // some logic involving b 

    for (vector::const_iterator it = v.begin; it != v.end(); ++it) 
    { 
     // significant logic involving b 
    } 

    // more logic involving b 
    return .... 
} 

Los detalles no son importantes, pero el uso de 'b' es omnipresente en la implementación.

Ahora, con plantillas, puede refactorizar un poco:

template <bool b> double innerLoop_B(vector<double> v) { ... same as before ... } 
double innerLoop(const bool b, const vector<double> & v) 
{ return b ? innerLoop_templ_B<true>(v) : innerLoop_templ_B<false>(v)); } 

Siempre que tenga una parte relativamente pequeña, discreta, un conjunto de valores para un parámetro Puede versiones automáticamente instantiate separadas para ellos.

Considerar las posibilidades cuando 'b' se basa en la detección de la CPU. Puede ejecutar un conjunto de código optimizado de forma diferente en función de la detección en tiempo de ejecución. Todo desde el mismo código fuente, o puede especializar algunas funciones para algunos conjuntos de valores.

Como ejemplo concreto, una vez vi un código que necesitaba fusionar algunas coordenadas enteras. El sistema de coordenadas 'a' era una de dos resoluciones (conocidas en tiempo de compilación), y el sistema de coordenadas 'b' era una de dos resoluciones diferentes (también conocida en tiempo de compilación). El sistema de coordenadas objetivo debe ser el múltiplo menos común de los dos sistemas de coordenadas de origen. Se utilizó una biblioteca para calcular el LCM en tiempo de compilación e instanciar el código para las diferentes posibilidades.

Cuestiones relacionadas