Metaprogramación con plantillas en C++

C++

El uso de plantillas (templates en inglés) en C++ nos permite salvar varias limitaciones del lenguaje, uno de los usos más conocidos es el empleado para implementar estructuras de datos, ya sea implementándolas nosotros mismos o usando aquellas provistas por STL, Boost, …

C++

Uso tradicional de las plantillas

Un ejemplo sencillo de plantilla para almacenar una tupla de valores cualquiera sería el siguiente:

template <class T>
class Tupla
{
public:
   Tupla(const T &_uno, const T &_dos) : uno(_uno), dos(_dos)
   {
   }
   const T &GetUno() const
   {
      return(uno);
   }
   const T &GetDos() const
   {
      return(dos);
   }
private:
   T uno;
   T dos;
};

El siguiente trozo de código es un ejemplo del uso de la plantilla:

#include <stdio.h>

int main(int argc, char **argv)
{
   Tupla<int> dato(1, 2);

   printf("Dato: %d, %d\n", dato.GetUno(), dato.GetDos());

   return(0);
}

Ejemplos de metaprogramación

Creo que será mucho más sencillo explicar que es la metaprogramación mediante ejemplos.

Optimizar la compilación

El siguiente trozo de código implementa un vector de datos de tamaño fijo (en tiempo de compilación) mediante plantillas:

template <class TIPO, unsigned int LONGITUD>
class Vector
{
public:
   Vector(const TIPO *_datos)
   {
      if (_datos != NULL)
      {
         for(unsigned int i = 0; i < LONGITUD; i++)
         {
            datos[i] = _datos[i];
         }
      }
   }
   Vector &operator +=(const Vector<TIPO, LONGITUD> &otro)
   {
      for(unsigned int i = 0; i < LONGITUD; i++)
      {
         datos[i] += otro.datos[i];
      }

      return(*this);
   }
   const TIPO &operator[](unsigned int indice) const
   {
      return(datos[indice]);
   }
   const unsigned int longitud() const
   {
      return(LONGITUD);
   }
private:
   TIPO datos[LONGITUD];
};

int main(int argc, char **argv)
{
   int datos1[] = {1, 2, 3, 4}, datos2[] = {2, 4, 6, 8};
   Vector<int, 4> vector1(datos1), vector2(datos2);

   vector1 += vector2;

   return(0);
}

La idea es hacer que el compilador sea capaz de desdoblar los bucles for de dentro de la implementación de la clase en sentencias simples, dado que el tamaño del vector se sabe en el momento de compilar:

datos[0] += otro.datos[0];
datos[1] += otro.datos[1];
datos[2] += otro.datos[2];
datos[3] += otro.datos[3];

Algoritmo en tiempo de compilación

La implementación típica del cálculo del factorial de un número sería algo similar a lo siguiente:

unsigned int factorial(unsigned int numero)
{
   return((numero <= 0) ? 1 : (numero * factorial(numero - 1)));
}

int main(int argc, char **argv)
{
   printf("Factorial de 4: %u\n", factorial(4));

   return(0);
}

Esto podría metaprogramarse con plantillas del siguiente modo:

template <unsigned int N>
class Factorial
{
public:
   enum
   {
      VALOR = N * Factorial<N - 1>::VALOR
   };
};

// Esto es una especialización de la plantilla definida arriba, el precompilador la usará
// para el caso en el que el número sea cero, para el resto de valores se usa la de arriba.
template<>
class Factorial<0>
{
public:
   enum
   {
      VALOR = 1
   };
};

int main(int argc, char **argv)
{
   printf("Factorial de 4: %u\n", Factorial<4>::VALOR);

   return(0);
}

El precompilador resolverá de manera estática, es decir no en ejecución, el valor de Factorial<4>::VALOR por un 24 calculado del modo siguiente:

Factorial<4 * Factorial<3 * Factorial<2 * Factorial<1 * Factorial<0>::VALOR>::VALOR>::VALOR>::VALOR>::VALOR>::VALOR

Conclusiones

¿Cuál es la ventaja de la metaprogramación?, el código del ejemplo no es más legible que el tradicional, de hecho es bastante confuso y requiere de un conocimiento profundo del lenguaje para entenderlo, pero el código del ejemplo del factorial con plantillas se resuelve en compilación y no en ejecución, con lo que no requiere ningún tipo de cálculo, en realidad todo se hace en la etapa de precompilación, cuándo se substituyen todas las plantillas en C++.

Referencias

Deja un comentario