Qué son las plantillas en C++

Qué son las plantillas en C++

Las plantillas (templates) son uno de los grandes aportes de C++ al lenguaje C. Anteriormente se pasaban variables como parámetros de las funciones. El concepto de plantillas no se limita sólo a las funciones, sino que también puede ser utilizado en clases y estructuras.

¿Cuáles son algunas ventajas e inconvenientes?

Ventajas

El interés de las plantillas está 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 veremos a continuación, el uso de plantillas requiere tomar algunas precauciones (typename…).

  • El programa tarda 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, etc.).

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 más 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é se debe 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>, etc. 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. Pero si en otro programa se usa my_vector<my_vector<double> >, entonces se compilará únicamente my_vector<float> y my_vector<my_vector<float> >. Lo que debemos retener es que el compilador sabe 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. Hay que tener en cuenta la siguiente regla:

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

Observación: Puede ocurrir que un archivo que contiene una clase-plantilla tenga una extensión diferente a la de encabezado (.h o .hpp), por ejemplo .tcc. Esto es una convención y las puedes considerar como encabezados.

¿Qué es la convención de notaciones?

Generalmente, los parámetros de plantillas se escriben en mayúsculas, mientras que los otros tipos tiende a estar escritos en minúscula. En la práctica podemos escribirlos como queramos. Puedes escribirlos precedidos de una T. Por ejemplo, Tgraph para designar un parámetro de plantilla que representa un grafo. Esto vuelve el código más legible.

¿Cuáles son algunas plantillas conocidas?

STL

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)).

BGL

BGL (Boost Graph Library) proporciona clases de grafo genéricos y los algoritmos correspondientes (algoritmo del camino más corto, algoritmo de flot,etc.).

No se encuentra presente por defecto pero se instala fácilmente. Por ejemplo en Debian:

aptitude install libboost-graph-dev

¿Cómo trabajar con plantillas?

Para trabajar con plantillas necesitamos 4 cosas:

  1. 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 se debe tener en cuenta únicamente cuando se le instancia.
  2. 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 de ellos.

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 específico de las plantillas, sino que se aplica a las clases y estructuras en general. Y también a namespace. Por otro lado, es parecido al “/” 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. En la práctica 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 después de ser incluido en el programa principal.

¿Cuáles son las especificaciones de plantillas?

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

#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 ]

¿Para qué usar una 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;
}

A continuación 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_;

¿Cómo obtener los parámetros de plantilla, tipos y métodos estáticos de una clase-plantilla?

Una buena idea con las clases-plantilla es utilizar 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 aún 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:: depende de una parámetro de plantilla, debe ser precedido de un typename. Debido a que los tipos a veces se hacen difícil de manipular, es buena idea hacer typedef. Otro ejemplo más complicado:

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

¿Cuáles son algunos 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 bajo ya que concretamente se compila fact<5>, fact<4>... fact<0> justo para dar un ejemplo simple de template recurrente.

¿Cómo 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, daremos sólo un breve ejemplo:

#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));
 //...
};
Alrededor del mismo tema

Lenguajes