Bienvenidos sean a este post, hoy veremos como nos puede ayudar la metaprogramacion.
Metaprogramacion puede ser tratada como un paradigma de programacion. Es un enfoque completamente diferente con el proceso regular de programacion. Cuando nos referimos a regular, estamos hablando de los tres procesos habituales como son:
- codificacion
- compilacion
- ejecucion
Es obvio que el programa va a hacer lo que se supone que deba hacer cuando se ejecute. Como sabemos, un ejecutable es generado por el compilador a traves de la compilacion y linkeo, por otro lado la metaprogramacion es donde el codigo esta siendo ejecutado durante la compilacion del codigo. Pero esto puede hacer que surja una pregunta, como podemos ejecutar un codigo que aun no existe? Hace un tiempo hablamos sobre templates y como procesa la informacion al compilarlo. Al momento de compilarlo se hace con mas de un paso, en el primer paso el compilador define los tipos necesarios y los parametros que son usados en el template de clase o funcion. Con el siguiente paso, el compilador comienza a compilarlo de la manera que lo conocemos. Es decir, genera un codigo que sera enlazado por el linker para producir el archivo ejecutable final. Como la metaprogramacion sucede durante la compilacion del codigo, ya deberiamos hacernos una idea de cuales conceptos y constructos del lenguaje son usados, y con esto podemos deducir que todo aquello sea calculado al momento de compilacion puede ser como como un constructo de metaprogramacion, tales como los templates.
Para entender este concepto, vamos a analizar el ejemplo mas utilizado pero que tambien vuela mas mentes:
#include <iostream>
template <int N>
struct MetaFactoreo
{
enum {
valor = N * MetaFactoreo<N - 1>::valor
};
};
template <>
struct MetaFactoreo<0>
{
enum {
valor = 1
};
};
int main()
{
std::cout << MetaFactoreo<5>::valor << std::endl;
std::cout << MetaFactoreo<6>::valor << std::endl;
std::cout << MetaFactoreo<3>::valor << std::endl;
}
Primero declaramos un template generico donde recibimos solo valores de tipo int y en la constante del enum almacenamos el resultado de la ecuacion y para ello hacemos una recursion a la misma struct. Luego tenemos una sobrecarga donde se usara unicamente para cuando reciba el valor de cero y establece el valor de uno. En el main, tenemos tres llamados a la struct con tres valores distintos para que muestre el valor final de la propiedad. Compilemos y veamos como es la salida:
$ ./meta
120
720
6
$
Si vienen de posts anteriores donde hemos visto otras formas de implementar una ecuacion de factoreo, por que lo implementamos asi siendo que hay formas mas eficientes? La respuesta es simple, la eficiencia. Suena extraño pero ya veremos por que. Si bien de esta forma toma mas tiempo al momento de compilarlo, es mucho mas rapido comparado a la funcion normal de factoreo (ya sea recursiva o iterable) y por que es mas rapido que estos metodos? Porque este calculo se realiza al momento de la compilacion, y esto da como resultado que al momento de ejecucion se muestran el resultado de la operacion, y evitamos el calculo en el momento de ejecucion.
Vamos a descomponer y analizar mas en detalle el codigo anterior. Ya comentamos que MetaFactoreo tiene un solo enum con una propiedad llamada valor. Como dijimos este almacenara la recursion de los llamados al struct, y como mencionamos tambien este calculo se realiza al momento de la compilacion. Esto hace que cada vez que accedamos a la propiedad valor, estamos accediendo a la propiedad calculada. Como mencionamos anteriormente, se calcula la recursion mediante el llamado al struct con la operacion.
Como ya mencionamos, hacemos el llamado nuevamente y le pasamos el N – 1 lo cual es distinto a N. Por lo tanto, cada llamado a la recursion va a generar un nuevo tipo con el nuevo valor. Tomemos como ejemplo el ultimo llamado a la struct:
std::cout << MetaFactoreo<3>::valor << std::endl;
Esta hace tres llamados recursivos a la struct, veamos como lo interpreta el primer paso del compilador:
struct MetaFactoreo<3>
{
enum {
valor = 3 * MetaFactoreo<2>::valor
};
};
struct MetaFactoreo<2>
{
enum {
valor = 2 * MetaFactoreo<1>::valor
};
};
struct MetaFactoreo<1>
{
enum {
valor = 1 * MetaFactoreo<0>::valor
};
};
Esto equivale a cada llamado, observen como se va haciendo el calculo, y en el ultimo caso entra en accion la sobrecarga del template, pero en la segunda pasada nos creara el siguiente pseudocodigo:
struct MetaFactoreo<3>
{
enum {
valor = 3 * 2
};
};
struct MetaFactoreo<2>
{
enum {
valor = 2 * 1
};
};
struct MetaFactoreo<1>
{
enum {
valor = 1 * 1
};
};
En este caso, no solamente procede a reemplazar cada llamado con su valor sino que para este caso eliminara los struct innecesarios y se quedara solamente con el primero para devolver el resultado que corresponde, 6. Por lo tanto, al momento de ejecutarlo se transforma en la siguiente linea:
std::cout << 6 << std::endl;
Esta es la belleza de metaprogramacion, todo se hace en el momento de la compilacion y no deja una huella como si fuera un ninja. Como comentamos varias veces, al hacerse todo en la compilacion va a tardar un poco mas pero la ejecucion sera infinitamente mas rapida. Este tipo de meta-implementacion puede ser muy util para los casos de extremo calculos, como por ejemplo la secuencia Fibonacci.
En resumen, hoy hemos hablado sobre la metaprogramacion como paradigma, como se aplica, sus pros y contras con respecto a otros, asi como un ejemplo simple para verlo en accion y detalles de como lo calcula el compilador. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donation
It’s for maintenance of the site, thanks!
$1.50
