2009-06-11 22 views
15

Con referencia a this question, ¿podría alguien explicar y publicar el código de metaprogramación? Busqué en Google el término, pero no encontré ejemplos para convencerme de que pueda ser de utilidad práctica.¿Qué es la metaprogramación?

En la misma nota, ¿es Qt's Meta Object System una forma de metaprogramación?

JRH

+2

No soy un programador de QT, pero parece una forma de metaprogramación. Sin embargo, cuando los programadores de C++ hablan de metaprogramación, generalmente se refieren específicamente a la metaprogramación de plantillas (que no tiene nada que ver con el sistema de metaobjetos de QT). Existen varios otros sabores: algunos lenguajes le permiten hacer metaprogramación en tiempo de ejecución (manipulando o extendiendo tipos mientras el programa se está ejecutando), y el enfoque de QT parece generar literalmente nuevos archivos de código para ser #incluidos. Pero todo es metaprogramación, en el sentido de que un metaprograma es simplemente un programa que genera o manipula un programa. – jalf

+0

¿Está interesado en la metaprogramación general o solo en la metaprogramación de plantillas C++? –

Respuesta

24

La mayoría de los ejemplos hasta ahora han operado en valores (dígitos de computación de pi, el factorial de N o similar), y esos son más o menos ejemplos de libros de texto, pero en general no son muy útiles. Es difícil imaginar una situación en la que realmente necesite que el compilador compre el decimoséptimo dígito de pi. O lo codifica usted mismo o lo calcula en tiempo de ejecución.

Un ejemplo que podría ser más relevante para el mundo real podría ser la siguiente:

Digamos que tenemos una clase de matriz en donde el tamaño es un parámetro de plantilla (lo que esta sería declarar una matriz de 10 números enteros: array<int, 10>)

Ahora es posible que deseemos concatenar dos matrices, y podemos usar un poco de metaprogramación para calcular el tamaño de la matriz resultante.

template <typename T, int lhs_size, int rhs_size> 
array<T, lhs_size + rhs_size> concat(const array<T, lhs_size>& lhs, const array<T, rhs_size>& rhs){ 

    array<T, lhs_size + rhs_size> result; 
    // copy values from lhs and rhs to result 
    return result; 

} 

Un ejemplo muy simple, pero al menos los tipos tienen algún tipo de relevancia en el mundo real. Esta función genera una matriz del tamaño correcto, lo hace en tiempo de compilación y con seguridad de tipo completo. Y está computando algo que no podríamos haber hecho fácilmente, ya sea codificando los valores (podríamos concatenar muchas matrices con diferentes tamaños), o en tiempo de ejecución (porque perderíamos la información del tipo)

Más comúnmente, sin embargo, tiende a utilizar metaprogramación para tipos, en lugar de valores.

Un buen ejemplo se puede encontrar en la biblioteca estándar. Cada tipo de contenedor define su propio tipo de iterador, pero los indicadores antiguos simples pueden también ser utilizados como iteradores. Técnicamente, se requiere un iterador para exponer una cantidad de miembros typedef, como value_type, y los punteros obviamente no hacen eso. Así que usamos un poco de metaprogramación para decir "oh, pero si el tipo de iterador resulta ser un puntero, su value_type debería usar esta definición".

Hay dos cosas que se deben tener en cuenta al respecto. La primera es que estamos manipulando tipos, no valores. No estamos diciendo "el factorial de N es tal y tal", sino que "el value_type de un tipo T se define como ..."

Lo segundo es que se usa para facilitar la programación genérica. (Los iteradores no serían un concepto muy genérico si no funcionara con el ejemplo más simple, un puntero en una matriz. Por lo tanto, usamos un poco de metaprogramación para completar los detalles necesarios para que un puntero se considere válido iterador).

Este es un caso de uso bastante común para la metaprogramación. Claro, puede usarlo para una amplia gama de otros fines (las plantillas de expresión son otro ejemplo comúnmente utilizado, destinado a optimizar los cálculos caros, y Boost.Spirit es un ejemplo de ir por la borda y le permite definir su propio analizador en compilación- tiempo), pero probablemente el uso más común es suavizar estos pequeños baches y casos de esquina que de otro modo requerirían un manejo especial y hacer imposible la programación genérica.

+1

... y finalmente un ejemplo del mundo real - como se solicita :) – xtofl

+1

+1 Esperaba que alguien hubiera publicado un ejemplo del mundo real. – Tom

+0

De hecho, utilizo la metaprogramación de plantillas (TMP) para un tipo de cómputo de valor que uso con bastante frecuencia. Tengo un TMP simple que calcula la siguiente potencia de 2 mayor o igual que una constante. Lo uso para la selección de bin de power-of-2 en función del tamaño de la estructura en tiempo de ejecución. – Adisak

5

Aquí está un ejemplo común:

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

    template <> 
    struct fact<1> { 
     enum { value = 1 }; 
    }; 

    std::cout << "5! = " << fact<5>::value << std::endl; 

Básicamente, se está utilizando plantillas para calcular un factorial.

Un ejemplo más práctico que vi recientemente fue un modelo de objetos basado en tablas de bases de datos que utilizaban clases de plantilla para modelar relaciones de claves externas en las tablas subyacentes.

+3

La ventaja aquí es que el factorial 5 se calcula en tiempo de compilación. El problema con esto es que si lo haces cout << fact también se calcula en tiempo de compilación y dará como resultado tiempos de compilación más largos. Todavía es un buen truco – Glen

+0

Es improbable que el hecho se calculará el tiempo de compilación. AFAIK, los compiladores suelen tener un límite configurable de instanciación de clase de plantilla en caso de tales casos recursivos ... –

+2

Pero el ejemplo de POINT of Glen sigue siendo válido; cuanto más trabajo se está haciendo con la meta programación de plantillas, más largos serán los tiempos de compilación. –

2

El siguiente ejemplo se ha eliminado del excelente libro C++ Templates - The complete guide.

#include <iostream> 
using namespace std; 

template <int N> struct Pow3 { 
    enum { pow = 3 * Pow3<N-1>::pow }; 
} 

template <> struct Pow3<0> { 
    enum { pow = 1 }; 
} 

int main() { 
    cout << "3 to the 7 is " << Pow<7>::pow << "\n"; 
} 

El punto de este código es que el cálculo recursivo de la séptima potencia de 3 se lleva a cabo en tiempo de compilación en lugar de tiempo de ejecución. Por lo tanto, es extremadamente eficiente en términos de rendimiento en tiempo de ejecución, a expensas de una compilación más lenta.

¿Esto es útil? En este ejemplo, probablemente no. Pero hay problemas donde realizar cálculos en tiempo de compilación puede ser una ventaja.

+0

¿No debería ser Pow <7>? – Blindy

+0

Urgh - sí, debería - gracias. –

+2

¿No debería ser Pow3 <7>? –

7

El concepto proviene por completo del nombre Meta- significa abstraer de lo que está prefijado.
En más 'estilo conversacional' para hacer algo con la cosa en lugar de la cosa misma.

En este sentido, la metaprogramación es esencialmente escribir código, que escribe (o hace que se escriba) más código.

El sistema de plantillas C++ es metaprogramación ya que no hace simplemente sustitución textual (como hace el preprocesador c) sino que tiene un medio (complejo e ineficaz) de interactuar con la estructura de código que analiza para producir código que está lejos mas complejo. En este sentido, el preprocesamiento de plantilla en C++ está completo. Esto no es un requisito para decir que algo es la metaprogramación pero es casi seguro suficiente para contar como tal.

Las herramientas de generación de código que son parametrizables se pueden considerar metaprogramación si su lógica de plantilla es suficientemente compleja.

Cuanto más cerca de un sistema se trabaja con el árbol de sintaxis abstracta que representa el lenguaje (en oposición a la forma textual en que lo representamos), más probable es que se considere metaprogramación.

Al mirar el código QT MetaObjects no lo llamaría (a partir de una inspección somera) metaprogramación en el sentido generalmente reservado para cosas como el sistema de plantillas C++ o las macros Lisp.Parece ser simplemente una forma de generación de código que inyecta alguna funcionalidad en clases existentes en la etapa de compilación (puede verse como un precursor del tipo de estilo de programación orientado a aspectos actualmente en boga o los sistemas de objetos basados ​​en prototipos en lenguajes como JavaScripts).

Como ejemplo del tipo de longitudes extremas se puede tomar esto en C++ no es Boost MPL cuya tutorial muestra cómo se obtiene:

Dimensioned types (unidades de medida)

quantity<float,length> l(1.0f); 
quantity<float,mass> m(2.0f); 
m = l; // compile-time type error 

Higher Order Metafunctions

dos veces (f, x): = f (f (x))

template <class F, class X> 
struct twice 
    : apply1<F, typename apply1<F,X>::type> 
{}; 

struct add_pointer_f 
{ 
    template <class T> 
    struct apply : boost::add_pointer<T> {}; 
}; 

Ahora podemos utilizar dos veces con add_pointer_f para construir punteros a punteros:

BOOST_STATIC_ASSERT((
    boost::is_same< 
     twice<add_pointer_f, int>::type 
     , int** 
    >::value 
)); 
4

Otro ejemplo: en este caso, la técnica de metaprogramación se utiliza para obtener un valor de precisión arbitraria de PI en tiempo de compilación usando el algoritmo de Gauss-Legendre.

¿Por qué debería usar algo así en el mundo real? Por ejemplo, para evitar la repetición de cálculos, para obtener ejecutables más pequeños, para ajustar el código para maximizar el rendimiento en una arquitectura específica, ...

Personalmente me encanta la metaprogramación porque odio repetir cosas y porque puedo ajustar constantes explotando los límites de la arquitectura .

Espero que les guste.

Sólo mis 2 centavos.

/** 
* FILE  : MetaPI.cpp 
* COMPILE : g++ -Wall -Winline -pedantic -O1 MetaPI.cpp -o MetaPI 
* CHECK : g++ -Wall -Winline -pedantic -O1 -S -c MetaPI.cpp [read file MetaPI.s] 
* PURPOSE : simple example template metaprogramming to compute the 
*    value of PI using [1,2]. 
* 
* TESTED ON: 
* - Windows XP, x86 32-bit, G++ 4.3.3 
* 
* REFERENCES: 
* [1]: http://en.wikipedia.org/wiki/Gauss%E2%80%93Legendre_algorithm 
* [2]: http://www.geocities.com/hjsmithh/Pi/Gauss_L.html 
* [3]: http://ubiety.uwaterloo.ca/~tveldhui/papers/Template-Metaprograms/meta-art.html 
* 
* NOTE: to make assembly code more human-readable, we'll avoid using 
*  C++ standard includes/libraries. Instead we'll use C's ones. 
*/ 

#include <cmath> 
#include <cstdio> 

template <int maxIterations> 
inline static double compute(double &a, double &b, double &t, double &p) 
{ 
    double y = a; 
    a = (a + b)/2; 
    b = sqrt(b * y); 
    t = t - p * ((y - a) * (y - a)); 
    p = 2 * p; 

    return compute<maxIterations - 1>(a, b, t, p); 
} 

// template specialization: used to stop the template instantiation 
// recursion and to return the final value (pi) computed by Gauss-Legendre algorithm 
template <> 
inline double compute<0>(double &a, double &b, double &t, double &p) 
{ 
    return ((a + b) * (a + b))/(4 * t); 
} 

template <int maxIterations> 
inline static double compute() 
{ 
    double a = 1; 
    double b = (double)1/sqrt(2.0); 
    double t = (double)1/4; 
    double p = 1; 

    return compute<maxIterations>(a, b, t, p); // call the overloaded function 
} 

int main(int argc, char **argv) 
{ 
    printf("\nTEMPLATE METAPROGRAMMING EXAMPLE:\n"); 
    printf("Compile-time PI computation based on\n"); 
    printf("Gauss-Legendre algorithm (C++)\n\n"); 

    printf("Pi=%.16f\n\n", compute<5>()); 

    return 0; 
} 
+0

La función sqrt() no se ejecutará en tiempo de compilación, por lo que no se está computando el valor de PI en tiempo de compilación, ¿verdad? Simplemente está diseñando una profundidad de llamada de función recursiva en tiempo de compilación. –

8

Aunque es grande (2000loc) Hice un sistema de clases reflexiva dentro de C++ que es independiente e incluye compilador de clasificación de objetos y metadatos, pero no tiene gastos generales de almacenamiento o acceso penalizaciones de tiempo. Es una metaprogramación hardcore, y se usa en un gran juego en línea para mapear objetos de juego para transmisión de red y mapeo de bases de datos (ORM).

De todos modos toma un tiempo para compilar, unos 5 minutos, pero tiene la ventaja de ser tan rápido como el código ajustado a mano para cada objeto. Por lo tanto, ahorra mucho dinero al reducir el tiempo de CPU significativo en nuestros servidores (el uso de CPU es el 5% de lo que solía ser).

+1

Publicaré un artículo sobre el sistema en el futuro cercano, después de que lo aclare con nuestros abogados. –

+0

¡Suena interesante! : D Espero el artículo. –

+1

¿Alguna actualización de esta? Me interesaría mucho ver el código, ya que los metadatos generales no siempre son interesantes. – Xeo

2

Es difícil decir qué meta-programación C++ es. Cada vez más, siento que es muy similar a la introducción de 'tipos' como variables, en la forma en que lo tiene la programación funcional. Hace que la programación declarativa sea posible en C++.

Es mucho más fácil mostrar ejemplos.

Uno de mis favoritos es un 'truco' (o patrón :)) a Flatte multiplican anidado switch/case bloques:

#include <iostream> 
using namespace std; 

enum CCountry { Belgium, Japan }; 
enum CEra  { ancient, medieval, future }; 

// nested switch 
void historic(CCountry country, CEra era) { 
    switch(country) { 
     case(Belgium): 
      switch(era) { 
      case(ancient): cout << "Ambiorix"; break; 
      case(medieval): cout << "Keizer Karel"; break; 
      } 
      break; 
     case(Japan): 
      switch(era) { 
      case(future): cout << "another Ruby?"; break; 
      case(medieval): cout << "Musashi Mijamoto"; break; 
      } 
      break; 
    } 
} 


// the flattened, metaprogramming way 
// define the conversion from 'runtime arguments' to compile-time arguments (if needed...) 
// or use just as is. 
template< CCountry country, CEra era > void thistoric(); 


template<> void thistoric<Belgium, ancient>() { cout << "Ambiorix"; } 
template<> void thistoric<Belgium, medieval>() { cout << "Keizer Karel"; } 
template<> void thistoric<Belgium, future >() { cout << "Beer, lots of it"; } 

template<> void thistoric<Japan, ancient>() { cout << "wikipedia"; } 
template<> void thistoric<Japan, medieval>() { cout << "Musashi"; } 
template<> void thistoric<Japan, future >() { cout << "another Ruby?"; } 


// optional: conversion from runtime to compile-time 
// 
template< CCountry country > struct SelectCountry { 
    static void select(CEra era) { 
    switch (era) { 
      case(medieval): thistoric<country, medieval>(); break; 
      case(ancient ): thistoric<country, ancient >(); break; 
      case(future ): thistoric<country, future >(); break; 

    } 
    } 
}; 

void Thistoric (CCountry country, CEra era) { 
    switch(country) { 
      case(Belgium): SelectCountry<Belgium>::select(era); break; 
      case(Japan ): SelectCountry<Japan >::select(era); break; 
    } 
    } 



int main() { 
    historic(Belgium, medieval); // plain, nested switch 
    thistoric<Belgium,medieval>(); // direct compile time switch 
    Thistoric(Belgium, medieval);// flattened nested switch 
    return 0; 
} 
0

QtMetaObject básicamente implementa la reflexión (Reflection) y IS una de las principales formas de metaprogramación, bastante poderosa en realidad. Es similar al reflejo de Java y también se usa comúnmente en lenguajes dinámicos (Python, Ruby, PHP ...).Es más legible que las plantillas, pero ambos tienen sus pros y sus contras.

2

La única vez que necesitaba usar Boost.MPL en mi trabajo diario era cuando necesitaba convertir boost::variant ay desde QVariant.

Dado que boost::variant tiene un mecanismo de visita O (1), la dirección boost::variant a QVariant es casi trivial.

Sin embargo, QVariant no tiene un mecanismo de visitas, por lo que con el fin de convertirlo en un boost::variant, es necesario iterar sobre la mpl::list de tipos que el boost::variant instanciación específica puede contener, y para cada tipo preguntan si la QVariant contiene ese tipo, y si es así, extraiga el valor y devuélvalo en un boost::variant. Es bastante divertido, deberías probarlo :)

0

Este es un simple "cálculo de valor" en la línea de Factorial. Sin embargo, es uno que es mucho más probable que realmente use en su código.

La macro CT_NEXTPOWEROFTWO2 (VAL) usa la metaprogramación de plantillas para calcular la siguiente potencia de dos mayor o igual a un valor para valores conocidos en tiempo de compilación.

template<long long int POW2VAL> class NextPow2Helper 
{ 
    enum { c_ValueMinusOneBit  = (POW2VAL&(POW2VAL-1)) }; 
public: 
    enum { 
     c_TopBit      = (c_ValueMinusOneBit) ? 
      NextPow2Helper<c_ValueMinusOneBit>::c_TopBit : POW2VAL, 
     c_Pow2ThatIsGreaterOrEqual = (c_ValueMinusOneBit) ? 
      (c_TopBit<<1) : c_TopBit 
    }; 
}; 
template<> class NextPow2Helper<1> 
{ public: enum { c_TopBit = 1, c_Pow2ThatIsGreaterOrEqual = 1 }; }; 
template<> class NextPow2Helper<0> 
{ public: enum { c_TopBit = 0, c_Pow2ThatIsGreaterOrEqual = 0 }; }; 
// This only works for values known at Compile Time (CT) 
#define CT_NEXTPOWEROFTWO2(VAL) NextPow2Helper<VAL>::c_Pow2ThatIsGreaterOrEqual 
Cuestiones relacionadas