Archivo de la etiqueta: templates

Curso de plantillas en C++: qué son las plantillas y para qué sirven

Las plantillas en C++ son un mecanismo que nos pertite crear tipos y funciones dependientes de otros tipos y valores no definidos. El mejor modo de entenderlo es pensando en un formulario, por ejemplo, el típico formulario de una administración pública, donde hay un montón de texto y huecos que rellenar. Cada hueco recibe un valor. La plantilla es parecido, es definir un tipo o función donde dejamos huecos para ser rellenados más tarde.

Según wikipedia:

Templates are a feature of the C++ programming language that allows functions and classes to operate with generic types. This allows a function or class to work on many different data types without being rewritten for each one.

Fuente: Wikipedia

La utilidad principal de las plantillas es poder crear código genérico. Un caso típico son los contenedores (listas, arrays, mapas, etc.). Sabemos que en C hay dos formas de desarrollar listas:

  1. Almacenando la información con un puntero tipo void* o
  2. Haciendo una implementación nueva para cada tipo que necesitemos.

El primer modo tiene la ventaja de poder contener cualquier cosa, pero a cambio, no hay control de tipos. Esto podría implicar problemas a largo plazo si no se programa con cuidado.

El segundo mecanismo tiene la ventaja del control de tipado, pero incrementa de forma lineal el tiempo de desarrollo y mantenimiento. Si tenemos un problema en una función y está implentada para 10 tipos, habría que cambiarla 10 veces…

Otra de las cosas que nos ofrecen las plantillas es la capacidad de metaprogramar y realizar acciones en tiempo de compilación.

En las prómimas entradas vamos a ir viendo en detalle ejemplos de plantillas en C++03 y, finalmente, veremos qué cambios se han ido introduciendo las versiones modernas.

A vueltas con las plantillas

Ando haciendo pruebas últimamente con C++ y hay algo que ahora no soy capaz de hacer, que es que elija entre un método u otro dependiendo de si lo que se le pasa por parámetro es una función o un functor.

Concretamente tengo algo como esto:

template<typename T, typename Proxy = queue_proxy<T> >
class channel
{
public:
	template<typename Result>
	channel<Result, typename Proxy::template bind<Result>::type>
	operator>>(Result (&receiver)(T))
	{
		channel<Result, typename Proxy::template bind<Result>::type> out_channel;
		threads.push_back(new boost::thread(create_stream_channel_thread(*this, out_channel, receiver)));
		return channel<Result, typename Proxy::template bind<Result>::type>(out_channel);
	}
};

La idea es que este código permite hacer algo como lo siguiente:

int fac(int x)
{
	if(x < 1) return 1;
	return x * fac(x-1);
}
 
void show(int x)
{
	std::cout << x << std::endl;
}
 
/* some code here */
 
channel<int> ch;
 
/* Se redirige el canal a fac y la salida del factorial a show */
ch >> fac >> show;
 
/* more code here */

Sí, el código se parece mucho a Axum de Microsoft, que la idea es crear algo parecido para C++.

El caso es que el operador funciona bien cuando se pasan funciones, pero no funciona si le paso un functor, como podría ser el caso siguiente:

class functor
{
public:
	functor() {}
 
	int operator()(int a)
	{
		/* do something interesting */
	}
};
 
/* some code here */
 
channel<int> ch;
ch >> functor() >> show;

La cosa es que no sé como hacer para detectar que se me pasa un functor y qué parametros son los que tiene, aunque aún tengo que hacer alguna prueba más antes de dar por imposible la tarea.

Plantillas en C++

Recuerdo cuando empecé a aprender C++ que la cosa que menos entendía era el uso de plantillas. Debo reconocer que me ha costado mucho comprender como funciona porque era algo completamente diferente a lo que hasta entonces había visto.

Por otro lado, hemos visto en los últimos tiempos como gran cantidad de arquitecturas han copiado el modelo para implementarlo a su modo. Aquí es donde vemos los genéricos de Java y de .Net. El problema es que no tienen ni de cerca la misma potencia que el sistema de C++, pero que como ventaja consumen menos espacio de ejecutable.

Para entrar un poco en materia, comentar que las plantillas de C++ son como documentos en los que dejamos lineas en blanco y luego las rellenamos cuando ya sabemos qué queremos poner en ahí. Pero en C++ no ha líneas en blanco, estas tienen nombre.

La principal ventaja de este sistema es que permite programar clases genéricas, como pueda ser una clase lista que automáticamente se adapte al tipo que debe contener. La segunda ventaja es el rendimiento, por el mismo motivo anterior.

Para empezar a ver un poco como va la cosa vamos a ver un ejemplo sencillo de plantilla:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T, int S>
class array
{
private:
   T *ptr;
 
public:
   array()
   {
      ptr = new T[S];
   }
 
   T& operator[](int idx)
   {
      return T[idx];
   }
 
   int size()
   {
      return S;
   }
};
//</typename>

Como vemos, hemos utilizado la palabra reservada template para indicar que se trata de una plantilla y luego, entre los símbolos < y > hemos introducido la lista de elementos de la plantilla. Estos elementos son los tipos y nombres de los valores a sustituir. En este caso son T de tipo typename y S de tipo int. Esto significa que cada vez que dentro de la clase aparezca T deberá sustituirse por su corresponditente valor pasado en la plantilla y que necesariamente deberá ser un tipo de dato (por ejemplo un int, una clase, etc.) y que cada vez que aparezca S deberá sustituirse por su valor que deberá ser un número entero. Para utilizar la plantilla sólo deberemos hacer lo siguiente:

1
2
3
4
5
6
7
8
9
10
int main(void)
{
   using namespace std;
 
   array<int , 5> a;
   cout < < "Tamaño del array: " << a.size() << endl;
   a[1] = 5;
 
   return 0;
}

Como vemos, lo único que hemos hecho ha sido añadir al nombre del la clase la lista de argumentos. En este caso concreto, deberá devolver que el tamaño es 5, que es el indicado en el parámetro de la plantilla.

Si nos fijamos en bibliotecas como STL o Boost, hacen un uso intensivo de las plantillas. El principal motivo es que C++ está optimizado para funcionar con plantillas, así que se prefieren al uso de mecanismos como la herencia. Además, la herencia hace que las llamadas a métodos sean más lentos, por lo que también se evita. Es en este momento cuando nos podemos preguntar el cómo hace C++ entonces para garantizar que se implementan ciertas funcionalidades. Por ejemplo, si queremos tener una lista ordenada, nos interesa que los elementos de la lista puedan compararse entre ellos con el operador <=. Pues la respuesta es sencilla: en la clase de plantilla simplemente utilizamos ese método y en tiempo de compilación se comprueba si está o no. Por ejemplo:

1
2
3
4
5
6
template<typename T>
T& mayor(T& a, T& b)
{
   return a >= b ? a : b;
}
//</typename>

Hemos creado una función mayor que devuelve el mayor de dos elementos de tipo T. Los elementos de tipo T deben sobrecargar el operador >= para que funcione. Como se ve, no es necesario hacer herencia de ningún tipo, el compilador detecta automáticamente si está o no sobrecargado y dará el correspondiente error en caso necesario (si miramos el caso de Java o de .Net veremos como sí es necesario hacer herencia para que el mecanismo de genéricos funcione de este modo).

Debo decir que el tema de las plantillas está muy bien y se puede hacer muchas cosas interesantes, como la clase array, que funciona igual que un array y que tiene un rendimiento muy parecido (benditos métodos inline de C++).

Otro día, seguiré hablando sobre plantillas, que dan para mucho.