Las plantillas en C++

Julio 2017



Introducción


Las plantillas (Templates) son uno de los grandes aportes de C++ al lenguaje C.

Hasta antes de las plantillas, se pasaban variables como parámetros de las funciones. Gracias al concepto de plantilla, es posible pasar tipos como parámetros, y de este modo definir funciones genéricas. Pero el concepto de plantillas no se limita a las funciones, también puede ser utilizado en clases y estructuras.

Ventajas


El interés de las plantillas residen en:
-la generalización: desde el momento en que el tipo incluye todo lo que es utilizado en la función o clase-plantilla, es posible pasar cualquier tipo como parámetro.
-simplicidad: únicamente se codifica una función o clase sin importar el tipo pasado como parámetro, lo que hace que el mantenimiento del código sea más fácil.

Inconvenientes


-Como lo veremos a continuación, el uso de plantillas requiere tomar algunas precauciones (typename…)
-El programa demora más en ser compilado.

¿Cuándo utilizar plantillas?


El uso de plantillas es apropiado para definir contenedores, es decir estructuras que sirven para almacenar una colección de objetos (una lista, un vector, un grafo…)

Las plantillas también son apropiadas para definir algoritmos genéricos que se aplican a una familia de clase. Por ejemplo, es interesante codificar un algoritmo del camino mas corto, independientemente de la estructura del grafo. El uso de un functor puede ser apropiado para acceder a los pesos instalados sobre los arcos del grafo en este caso. La clase de grafo pasada como parámetro debe cumplir un cierto número de pre-requisitos para que el algoritmo pueda ser aplicado. Si no, el programa no compilará…

Qué debo poner en las .hpp y .cpp


Al ser C++ un lenguaje compilado, está claro que no podemos compilar para una función o clase dada todas sus versiones. Por ejemplo, si definimos una clase-plantilla de vector, que llamaremos my_vector<T>, no podemos compilar my_vector<int>, my_vector<char>, my_vector<my_struct> ...sabiendo que hay una infinidad de tipos pudiendo ser pasados como parámetros.

Por ello, una clase-plantilla es (re)compilada para cada tipo de instancia presente en el programa. Así, si en mi programa utilizo my_vector<int> y my_vector<char>, únicamente estas versiones serán compiladas. Si en otro programa utilizo my_vector<my_vector<double> >, compilaré únicamente my_vector<float> y my_vector<my_vector<float> >. Lo que debemos retener, es que el compilador se las arregla solo para saber qué versión debe compilar.

De lo que acabamos de decir, se deduce que una función o clase-plantilla no puede ser “precompilado”, ya que es compilado para cada instancia. Por lo tanto retendremos la regla siguiente:

Si una función o clase-plantilla es utilizada únicamente en un .cpp (archivo fuente), entonces puede ser implementada en este .cpp. Si no, debe ser implementada en un .hpp (encabezado).

Observación:

Puede ocurrir que un archivo conteniendo una clase-plantilla tenga una extensión diferente a la de encabezado (.h o .hpp), por ejemplo .tcc. Esto es únicamente una convención. Personalmente, yo las considero como encabezados.

Convención de notaciones


Los parámetros de plantillas son generalmente escritos en mayúsculas (mientras que los otros tipos son generalmente escritos en minúscula). En la práctica, podemos escribirlos como queramos. Personalmente los escribo precedidos de una T (por ejemplo Tgraph para designar un parámetro de plantilla que representa un grafo).

Esto puede parecer inútil pero veremos que es muy práctico con los typenames ya que vuelve el código más legible.

Algunas plantillas conocidas


STL


La STL (Standard Template Library) viene por defecto con los compiladores C++. Esta biblioteca incluye un juego de contenedores genéricos, especialmente:
std::vector: vectores (tabla de elementos de tipo T adyacentes en memoria), acceso en O(1).
std::set: conjuntos de elementos de tipo T sin repeticiones y ordenados según el operador <, acceso en O(log(n))
std::list: listas encadenadas (acceso en O(n), inserción al inicio y al final de la lista en O(1))

Para hacerse una idea del contenido de la STL, hacer clic en el siguiente enlace:
http://www.sgi.com/tech/stl/

BGL


La BGL (Boost Graph Library) proporciona clases de grafo genéricos y los algoritmos correspondientes (algoritmo del camino más corto, algoritmo de flot…). Para hacerse una idea del contenido de la BGL, hacer clic en el siguiente enlace.
http://www.boost.org/doc/libs/1_35_0/libs/graph/doc/table_of_contents.html

Esta no esta presente por defecto pero se instala fácilmente. Por ejemplo bajo Debian:

aptitude install libboost-graph-dev

Trabajar con plantillas


Para trabajar con plantillas, necesitamos 4 cosas:

-La palabra clave typename: indica que el tipo que sigue es abstracto (parámetro de plantilla o depende de un parámetro de plantilla) y debe ser tomado en cuenta únicamente cuando se le instancia.

-La palabra clave template: indica que la clase o función que le sigue toma parámetros de plantilla. Después de la palabra clave template se escriben directamente los parámetros de plantilla (precedidos de la palabra clave typename, struct, class, o tipo de base según el tipo de parámetro de plantilla esperado) entre “<< >>”, seguidos de la clase o función.

En el ejemplo que sigue veremos:
-cómo codificar una clase-plantilla
-cómo codificar una función-plantilla
-cómo codificar un operador-plantilla
En este ejemplo las clases o funciones-plantilla toman solo un parámetro de plantilla, pero el procedimiento será el mismo con varios parámetros de plantilla.

Ejemplo:

template <typename T1, typename T2, ... >
type_devuelve_t mi_funcion(param1_t p1,param2_t p2, ...){
...
}

template <typename T1, typename T2, ... >
class mi_clase_t{
...
};

template <typename T1, typename T2, ... >
struct mi_struct_t{
...
};


-El operador :: : permite acceder a los campos (en particular los tipos) y métodos estáticos de una clase o de una estructura. No es especifico a las plantillas (se aplica a las clases y estructuras en general y a los namespaces). Es un poco como el “/” de los directorios. Así,
std::vector<int>::const_iterator significa que accedo al tipo const_iterator, almacenado en la clase vector<int>, ella misma codificada en el namespace std.

Vamos a definir nuestra propia clase de vector a fin de ilustrar lo que acabamos de decir. Evidentemente en la practica utilizaremos directamente la clase std::vector de la STL...


#include <iostream>
//----------------------- inicio my_vector_t.hpp
#include <cstdlib>
#include <ostream>

// Una clase-plantilla que toma un parámetro
template <typename T>
class my_vector_t{
	protected:
		unsigned tamaño; // almacena el tamaño del vector
		T *data;	// almacena los componentes del vector
	public:
		// El constructor
		my_vector_t(unsigned tamaño0 = 0,const T & x0 = T()):
			tamaño(tamaño0),
			data((T *)malloc(sizeof(T)*tamaño0))
		{
			for(unsigned i=0;i<taille;<gras>i) data[i] = x0;
		}

		// El destructor
		~my_vector_t(){
			free(data);
		}

		// Devuelve el tamaño del vector
		inline unsigned size() const{
			return tamaño;
		}

		// Un accesador en solo lectura sobre la iesima casilla del vector
		inline const T & operator[](unsigned i) const{
			if(i >= size()) throw;
			return data[i];
		}

		// Un accesador en lectura escritura sobre la iesima casilla del vector
		inline T & operator[](unsigned i){
			if(i >= size()) throw;
			return data[i];
		}
};

// Un operador-plantilla
template <typename T>
std::ostream & operator<<(std::ostream & out,const my_vector_t<T> & v){
	unsigned n = v.size();
	out << "[ ";
	for(unsigned i=0;i<n;<gras>i) out << v[i] << ' '; 
	out << ']';	
	return out;
}

//----------------------- fin my_vector_t.hpp

// Una función-plantilla
template <typename T>
void escribir(const my_vector_t<T> & v){
	unsigned n = v.size();
	std::cout << "[ ";
	for(unsigned i=0;i<n;<gras>i) std::cout << v[i] << ' '; 
	std::cout << ']';
}

int main(){
	my_vector_t<int> v(5); // un vector de 5 enteros
	v[0] = 6; v[1] = 2; v[2] = 3; v[3] = 4; v[4] = 8;
	escribir<int>(v); // llamado a la función template
	std::cout << std::endl;
	escribir(v); // llamado implícito de escribir<int>
	std::cout << std::endl;
	std::cout << v << std::endl; // llamado al operador template
	return 0;
}


Al ejecutar:

[ 6 2 3 4 8 ]
[ 6 2 3 4 8 ]
[ 6 2 3 4 8 ]


Todo lo que está entre “inicio clase my_vector_t" y "fin clase my_vector_t" podría ser desplazado a un encabezado (por ejemplo my_vector.hpp) luego ser incluido por el programa principal.

Especificaciones de plantillas


Nada impide que se implemente específicamente una clase o función para un conjunto de parámetros de plantilla. Notemos que no es necesario especificar todos los parámetros de plantilla. Partiendo del ejemplo precedente:


#include "my_vector.hpp"

// Una especificación de plantilla
void escribir(const my_vector_t<int> & v){
    unsigned n = v.size();
    std::cout << "{ ";
    for(unsigned i=0;i<n;<gras>i) std::cout << v[i] << ' ';
    std::cout << '}';
}

int main(){
    my_vector_t<int> v(5); // un vector de 5 enteros
    v[0] = 6; v[1] = 2; v[2] = 3; v[3] = 4; v[4] = 8;
    escribir<int>(v); // invoca a la función template
    std::cout << std::endl;
    escribir(v); // invoca a escribir (prevalece a la invocación implícita de escribir<int>)
    std::cout << std::endl;
    std::cout << v << std::endl; // invoca el operador template
    return 0;
}


Al ejecutarlo:

[ 6 2 3 4 8 ]
{ 6 2 3 4 8 }
[ 6 2 3 4 8 ]

Plantilla por defecto


También es posible precisar un parámetro de plantilla por defecto del mismo modo que con un parámetro de función.

Por ejemplo:

template<typename T = int>
class my_vector_t{
   //...
};

int main(){
  my_vector<> v; // un vector de int
  return 0;
}



Algunos ejemplos conocidos de plantillas por defecto: en la STL el functor de comparación, utilizado en las std::set, es inicializado por defecto por std::less (functor de comparación basado en <). De este modo podemos escribir indistintamente:

std::set<int> s;
std::set<int,std::less<int> > s_;

Obtener los parámetros de plantilla, tipos y métodos estáticos de una clase-plantilla


Una buena idea con las clases-plantilla, es poner typedef (en public) para poder obtener fácilmente los parámetros de plantilla. Ejemplo: tenemos una clase c1 <T> y deseamos obtener el tipo T. Esto será posible gracias a typedef y typename.

template <typename T>
struct my_class_t{
	typedef T data_t;
};

int main(){
	typedef my_vector_t<int>::data_t data_t; // Esto es un entero
}


Sin embargo únicamente podemos aplicar el operador :: si un miembro de la izquierda no es un tipo abstracto (dependiente de un tipo plantilla aun no evaluado). Por ejemplo, si deseamos manipular el typedef "const_iterator" de la clase std::vector proporcionada por la STL, si los parámetros de plantilla de std::vector no son asignados, el programa rechazará la compilación.

void escribir(const std::vector<int> & v){
	std::vector<int>::const_iterator vit(v.begin(),vend(v.end());
	for(;vit!=vend;<gras>vit) std::cout << *vit << ' ';
}
template <typename T>
void escribir(const std::vector<int> & v){
	std::vector<T>::const_iterator vit(v.begin(),vend(v.end()); // ERROR !
	for(;vit!=vend;<gras>vit) std::cout << *vit << ' ';
}


Aquí std::vector<T> está situado a la izquierda de :: y depende de una parámetro de plantilla. Es aquí que typename entra en juego.

template <typename T>
void escribir(const std::vector<int> & v){
	typename std::vector<T>::const_iterator vit(v.begin(),vend(v.end());
	for(;vit!=vend;<gras>vit) std::cout << *vit << ' ';
}


Debemos recordar que cuando el tipo a la izquierda de un :: depende de una parámetro de plantilla, debe ser precedido de un typename. Debido a que los tipos rápidamente se hacen difícil de manipular, es buena idea hacer typedef. En otro ejemplo, más complicado, esto da por ejemplo:

typedef typename std::vector<typename std::vector<T>::const_iterator>
::const_iterator mi_tipo_extraño_t

Templates recurrentes


Es posible definir plantillas recurrentes. Un ejemplo:

 #include <iostream>

template <int N>
int fact(){
	return N*fact<N-1>();
}

template <>
int fact<0>(){
	return 1;
}

int main(){
	std::cout << fact<5>() << std::endl;
	return 0;
}


Aquí el interés es bastante moderado ya que concretamente se compila fact<5>, fact<4>... fact<0>, es justo para dar un ejemplo simple de template recurrente.

Testear valores de tipo plantilla


Con boost es posible verificar si un tipo plantilla corresponde a un tipo esperado y bloquear la compilación si es así. Debido a que se utiliza la biblioteca boost, daré solo un ejemplo breve:

#include <boost/type_traits/is_same.hpp>
#include <boost/static_assert.hpp>

template <typename T>
struct mi_struct_que_debe_compilar_solo_si_T_es_int{
	BOOST_STATIC_ASSERT((boost::is_same<T,int>::value));
	//...
};

Enlaces utiles


Presentación de la STL (Standard Template Library): ofrece numerosos contenedores de plantilla:
http://www.sgi.com/tech/stl/
[ Instroduccion a la STL en C<gras>]

Presentación de la BGL (Boost Graph Library): una parte de la biblioteca boost, ofrece clases y algoritmos de grafos de plantillas:
http://www.boost.org/doc/libs/1_35_0/libs/graph/doc/table_of_contents.html

PD: El artículo original fue escrito por mamiemando, contribuidor de CommentCaMarche

Consulta también

Publicado por Carlos-vialfa. Última actualización: 2 de junio de 2009 a las 23:26 por Carlos-vialfa.
El documento «Las plantillas en C++» se encuentra disponible bajo una licencia Creative Commons. Puedes copiarlo o modificarlo libremente. No olvides citar a CCM (es.ccm.net) como tu fuente de información.