Bienvenidos sean a este post, hoy veremos una de las etapas de TMP.
Habitualmente, si tenemos la posibilidad de conocer los valores de entrada y salida de una tarea al momento de compilarlo, podemos usar metaprogramacion para la computacion durante la compilacion y esto nos ahorrara tiempo de ejecucion y consumo de memoria. Lo cual resulta especialmente util cuando trabajamos en proyectos que utilizan intensamente el CPU. Para entender este concepto,, vamos a analizar un ejemplo:
#include <iostream>
int f1(const int n)
{
return (n<=1) ? 1 : n * f1(n - 1);
}
constexpr int f2(const int n)
{
return (n<=1) ? 1 : n * f2(n - 1);
}
int main()
{
int a1 = f1(10);
int a2 = f2(10);
const int a3 = f2(10);
std::cout << "a1: " << a1 << "\ta2: " << a2;
std::cout << "\ta3: " << a3 << std::endl;
return 0;
}
Este es un codigo para calcular el factoreo de un numero. Para ello usaremos una funcion recursiva donde recibira un valor y devuelve 1 si el valor ingresado es menor o igual a 1. En caso contrario, devuelve la multiplicacion del valor recibido por la funcion con el valor anterior menos uno. Esto lo hara mientras no se cumpla la condicion anterior. La siguiente funcion hace exactamente lo mismo pero mediante el constexpr se trabaja al momento de compilacion o ejecucion dependiendo como se lo utilice, sobre constexpr hablamos en este post, pero eso lo veremos en el main. Mencionando al main, pasemos a hablar sobre este. Primero definiremos tres variables que almacenaran el resultado de las dos funciones anteriores. En el caso de a1 y a2 se ejecutaran en el momento de ejecucion, perdon por la redundancia, como dijimos anteriormente esta se adapta al momento de ser utilizado. En cambio, la tercer variable se ejecutara al momento de compilacion, para finalmente mostrar los tres valores obtenidos. Compilemos y veamos como es la salida:
$ ./fact
a1: 3628800 a2: 3628800 a3: 3628800
$
Con nuestro codigo funcionando, pasemos a convertirlo a metaprogramacion. Para ello tomaremos el codigo anterior y lo modificaremos de la siguiente manera:
#include <iostream>
template <int n>
struct fact
{
const static int valor = n * fact<n - 1>::valor;
};
template<>
struct fact<0>
{
const static int valor = 1;
};
int main()
{
std::cout << "fact<0>=" << fact<0>::valor << std::endl;
std::cout << "fact<1>=" << fact<1>::valor << std::endl;
std::cout << "fact<10>=" << fact<10>::valor << std::endl;
return 0;
}
Primero definiremos un template donde manejara solo el tipo int. En este estableceremos un struct que se encargara del factoreo. Este contendra una variable estatica que almacenara la recursion de multiplicarse el argumento recibido por la struct pero le pasaremos un valor menor al anterior y usara el valor actual en la variable. Es exactamente lo mismo que vimos en el codigo anterior que inclusive funcionara con el valor de 1, pero que sucedera con el 0? Para ello, aplicamos la especializacion que sigue a la struct anterior. En este, le indicamos que solo trabajara cuando reciba el valor de 0 y asignara el valor de 1 a la variable. En el main, mostraremos a valor dentro de fact y le pasaremos tres valores representativos como son 0, 1 y 10. Los dos primeros porque devuelven un valor en comun y el ultimo porque sera el verdadero factoreo. Compilemos y veamos como es la salida:
$ ./fact
fact<0>=1
fact<1>=1
fact<10>=3628800
$
Como pueden ver funciono perfectamente y tenemos la recursion propia en la variable del struct que nos permite obtener el valor pero esto tiene un par de inconvenientes, modifiquemos al main de la siguiente manera:
int main()
{
int m = 5;
std::cout << "fact<0>=" << fact<0>::valor << std::endl;
std::cout << "fact<1>=" << fact<1>::valor << std::endl;
std::cout << "fact<10>=" << fact<10>::valor << std::endl;
std::cout << "fact<m>=" << fact<m>::valor << std::endl;
return 0;
}
Agregamos una variable con un valor, y al final mostraremos el valor de usar al struct con la variable anterior, compilemos y veamos como es la salida:
$ g++ fact.cpp -o fact
fact.cpp: In function ‘int main()’:
fact.cpp:21:42: error: the value of ‘m’ is not usable in a constant expression
21 | std::cout << "fact<m>=" << fact<m>::valor << std::endl;
| ^
fact.cpp:17:13: note: ‘int m’ is not const
17 | int m = 5;
| ^
fact.cpp:21:42: note: in template argument for type ‘int’
21 | std::cout << "fact<m>=" << fact<m>::valor << std::endl;
| ^
$
Falla porque nosotros debemos usar valores constantes para los argumentos non-type. Para poder solucionarlo, debemos modificar a la linea de la variable de la siguiente manera:
const int m = 5;
El resto del codigo sigue siendo el mismo, compilemos y veamos como es la salida ahora:
$ ./fact
fact<0>=1
fact<1>=1
fact<10>=3628800
fact<m>=120
$
Como pueden ver funciono perfectamente y ocurrio lo que comentamos cuando hablamos de los argumentos non-type, sobre este tema hablamos en este post, y antes de finalizar vamos a comparar las funciones constexpr (FC) con TMP:
- Tiempo de computacion: FC se ejecuta al momento de compilacion o ejecucion, dependiendo del uso. TMP solo es en ejecucion.
- Listas de argumentos: FC toma solo valores pero TMP puede tomar valores y parametros de tipos
- Estructura de control: FC puede usar recursion, condiciones y bucles pero TMP usa solo recursion
En resumen, hoy hemos visto a la etapa de computacion en la compilacion, que es, como se aplica, y unos ejemplos practicos para verlo en accion. 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.


Donatión
It’s for site maintenance, thanks!
$1.50
