40 Plantillas

Según va aumentando la complejidad de nuestros programas y sobre todo, de los problemas a los que nos enfrentamos, descubrimos que tenemos que repetir una y otra vez las mismas estructuras.

Por ejemplo, a menudo tendremos que implementar arrays dinámicos para diferentes tipos de objetos, o listas dinámicas, pilas, colas, árboles, etc.

El código es similar siempre, pero estamos obligados a rescribir ciertas funciones que dependen del tipo o de la clase del objeto que se almacena.

Las plantillas (templates) nos permiten parametrizar estas clases para adaptarlas a cualquier tipo de dato.

Vamos a desarrollar un ejemplo sencillo de un array que pueda almacenar cualquier objeto. Aunque más adelante veremos que se presentan algunas limitaciones y veremos cómo solucionarlas.

Con lo que ya sabemos, podemos crear fácilmente una clase que encapsule un array de, por ejemplo, enteros. Veamos el código de esta clase:

// TablaInt.cpp: Clase para crear Tablas de enteros
// C con Clase: Marzo de 2002

#include <iostream>
using namespace std;

class TablaInt {
  public:
   TablaInt(int nElem);
   ~TablaInt();
   int& operator[](int indice) { return pInt[indice]; }

  private:
   int *pInt;
   int nElementos;
};

// Definición:
TablaInt::TablaInt(int nElem) : nElementos(nElem) {
   pInt = new int[nElementos];
}

TablaInt::~TablaInt() {
   delete[] pInt;
}

int main() {
   TablaInt TablaI(10);

   for(int i = 0; i < 10; i++)
      TablaI[i] = 10-i;

   for(int i = 0; i < 10; i++)
      cout << TablaI[i] << endl;

   return 0;
}

Ejecutar este código en OnlineGDB.

Bien, la clase TablaInt nos permite crear arrays de la dimensión que queramos, para almacenar enteros. Quizás pienses que para eso no hace falta una clase, ya que podríamos haber declarado sencillamente:

   int TablaI[10];

Bueno, tal vez tengas razón, pero para empezar, esto es un ejemplo sencillo. Además, la clase TablaInt nos permite hacer cosas como esta:

   int elementos = 24;
   TablaInt TablaI(elementos);

Recordarás que no está permitido usar variables para indicar el tamaño de un array. Pero no sólo eso, en realidad esta podría ser una primera aproximación a una clase TablaInt que nos permitiría aumentar el número elementos o disminuirlo durante la ejecución, definir constructores copia, o sobrecargar operadores suma, resta, etc.

La clase para Tabla podría ser mucho más potente de lo que puede ser un array normal, pero dejaremos eso para otra ocasión.

Supongamos que ya tenemos esa maravillosa clase definida para enteros. ¿Qué pasa si ahora necesitamos definir esa clase para números en coma flotante?. Podemos cortar y pegar la definición y sustituir todas las referencias a int por float. Pero, ¿y si también necesitamos esta estructura para cadenas, complejos, o para la clase persona que implementamos en anteriores capítulos?, ¿haremos una versión para cada tipo para el que necesitemos una Tabla de estas características?.

Afortunadamente existen las plantillas y (aunque al principio no lo parezca), esto nos hace la vida más fácil.

Sintaxis

C++ permite crear plantillas de funciones y plantillas de clases.

La sintaxis para declarar una plantilla de función es parecida a la de cualquier otra función, pero se añade al principio una presentación de la clase que se usará como referencia en la plantilla:

template <class|typename <id>[,...]>
<tipo_retorno> <identificador>(<lista_de_parámetros>)
{
   // Declaración de función
};

La sintaxis para declarar una plantilla de clase es parecida a la de cualquier otra clase, pero se añade al principio una presentación de la clase que se usará como referencia en la plantilla:

template <class|typename <id>[,...]>
class <identificador_de_plantilla>
{
   // Declaración de funciones
   // y datos miembro de la plantilla
};
Nota:

La lista de clases que se incluye a continuación de la palabra reservada template se escriben entre las llaves "<" y ">", en este caso esos símbolos no indican que se debe introducir un literal, sino que esos caracteres deben escribirse, no me es posible mostrar estos símbolos en negrita, por eso los escribo en rojo. Siento si esto causa algún malentendido.

Pero seguro que se ve mejor con un ejemplo:

template <class T1>
class Tabla {
  public:
   Tabla();
   ~Tabla();
   ...
};

Del mismo modo, cuando tengamos que definir una función miembro fuera de la declaración de la clase, tendremos que incluir la parte del template y como nombre de la clase incluir la plantilla antes del operador de ámbito (::). Por ejemplo:

template <class T1>
Tabla<T1>::Tabla() {
  // Definición del constructor
}

Plantillas de funciones

Un ejemplo de plantilla de función puede ser esta que sustituye a la versión macro de max:

template <class T>
T max(T x, T y) {
   return (x > y) ? x : y;
};

La ventaja de la versión en plantilla sobre la versión macro se manifiesta en cuanto a la seguridad de tipos. Por supuesto, podemos usar argumentos de cualquier tipo, no han de ser necesariamente clases. Pero cosas como estas darán error de compilación:

int a=2;
char b='j';

int c=max(a,b);

El motivo es que a y b no son del mismo tipo. Aquí no hay promoción implícita de tipos. Sin embargo, están permitidas todas las combinaciones en las que los argumentos sean del mismo tipo o clase, siempre y cuando que el operador > esté implementado para esa clase.

int a=3, b=5, c;
char f='a', g='k', h;
c = max(a,b);
h = max(f,g);

Plantilla para Tabla

Ahora estamos en disposición de crear una plantilla a partir de la clase Tabla que hemos definido antes. Esta vez podremos usar esa plantilla para definir Tablas de cualquier tipo de objeto.

template <class T>
class Tabla {
  public:
   Tabla(int nElem);
   ~Tabla();
   T& operator[](int indice) { return pT[indice]; }

  private:
   T *pT;
   int nElementos;
};

// Definición:
template <class T>
Tabla<T>::Tabla(int nElem) : nElementos(nElem) {
   pT = new T[nElementos];
}

template <class T>
Tabla<T>::~Tabla() {
   delete[] pT;
}

Dentro de la declaración y definición de la plantilla, podremos usar los parámetros que hemos especificado en la lista de parámetros del template como si se tratase de comodines. Más adelante, cuando creemos instancias de la plantilla para diferentes tipos, el compilador sustituirá esos comodines por los tipos que especifiquemos.

Y ya sólo nos queda por saber cómo declarar Tablas del tipo que queramos. La sintaxis es:

<identificador_de_plantilla><<tipo/clase>> <identificador/constructor>;

Seguro que se ve mejor con un ejemplo, veamos como declarar Tablas de enteros, punto flotante o valores booleanos:

Tabla<int>   TablaInt(32);   // Tabla de 32 enteros
Tabla<float> TablaFloat(12); // Tabla de 12 floats
Tabla<bool>  TablaBool(10);  // Tabla de 10 bools

Pero no es este el único modo de proceder. Las plantillas admiten varios parámetros, de modo que también podríamos haber especificado el número de elementos como un segundo parámetro de la plantilla:

template <class T, int nElementos>
class Tabla {
  public:
   Tabla();
   ~Tabla();
   T& operator[](int indice) { return pT[indice]; }

  private:
   T *pT;
};

// Definición:
template <class T, int nElementos>
Tabla<T,nElementos>::Tabla() {
   pT = new T[nElementos];
}

template <class T, int nElementos>
Tabla<T, nElementos>::~Tabla() {
   delete[] pT;
}

La declaración de tablas con esta versión difiere ligeramente:

Tabla<int,32>   TablaInt;   // Tabla de 32 enteros
Tabla<float,12> TablaFloat; // Tabla de 12 floats
Tabla<bool,10>  TablaBool;  // Tabla de 10 bools

Esta forma tiene una limitación: el argumento nElementos debe ser una constante, nunca una variable. Esto es porque el valor debe conocerse durante la compilación del programa. Las plantillas no son definiciones de clases, sino plantillas que se usan para generar las clases que a su vez se compilarán para crear el programa:

#define N 12
...
const n = 10;
int i = 23;

Tabla<int,N>     TablaInt;   // Legal, Tabla de 12 enteros
Tabla<float,3*n> TablaFloat; // Legal, Tabla de 30 floats
Tabla<char,i>    TablaFloat; // Ilegal

Ejecutar este código en OnlineGDB.

Ficheros de cabecera

En los siguientes ejemplos usaremos varios ficheros fuente. Más concretamente, crearemos algunos ficheros para definir plantillas que usaremos en programas de ejemplo.

Dado que algunas de esas plantillas se podrán usar en varios programas diferentes, es buena idea definir cada una en un fichero separado, que se pueda incluir desde otros ficheros fuente para hacer uso de esas plantillas.

Generalmente, un fichero de cabecera suele contener sólo declaraciones, de clases, tipos y/o funciones. En el caso de plantillas, también se incluye el código, pero esto es sólo en apariencia.

Es un error ver la implementación de las plantillas como código, ya que el compilador usa las plantillas para generar código que posteriormente compila, pero el código de una plantilla no es compilable. En realidad es sólo un molde para crear clases, y esas son las clases que se pueden compilar.

De modo que el código de una plantilla se suele escribir en el mismo fichero de cabecera que el resto de la declaración de plantilla.

Esta filosofía, la de crear ficheros de cabecera, es la que se debe seguir para reutilizar el código. Si hemos diseñado una clase o la plantilla de una clase que nos puede ser útil en otros proyectos, es buena idea convertirla en un fichero de cabecera, y llegado el caso, en una biblioteca externa.

Hay una precaución que se debe tomar siempre cuando se escriben ficheros de cabecera.

Debido a que el mismo fichero de cabecera puede ser incluido desde varios ficheros fuente de un mismo proyecto, es imprescindible diseñar un mecanismo que evite que se declaren las mismas clases, tipos o funciones varias veces. Esto provocaría errores de compilación, y harían imposible compilar el proyecto completo.

Esto es secillo, nos basamos en si una macro está o no definida, y haremos una compilación condicional del código en función de que la macro esté o no definida. Es importante elegir un nombre único para la macro. Lo normal es crear un identificador en función del nombre del fichero de cabecera.

Por ejemplo, si tenemos un fichero llamado "ccadena.h", el identificador de la macro podría ser CCADENA_H, o _CCADENA_H, o CCADENA, etc.

El fichero tendría esta estructura:

// Zona de comentarios, fechas, versiones, autor y propósito del fichero
#ifndef CCADENA
#define CCADENA
// Zona de código
#endif

De este modo, la primera vez que se incluye este fichero la macro CCADENA no está definida. Lo primero que hacemos es definirla, y después incluimos el código del fichero de cabecera.

En posteriores inclusiones la macro sí está definida, de modo que no se incluye nada de código, y no necesitamos definir la macro, ya que ya lo estaba.

Ejemplo de uso de plantilla Tabla

Vamos a ver un ejemplo completo de cómo aplicar la plantilla anterior a diferentes tipos.

Fichero de cabecera que declara y define la plantilla Tabla:

// Tabla.h: definición de la plantilla tabla:
// C con Clase: Marzo de 2002

#ifndef T_TABLA
#define T_TABLA

template <class T>
class Tabla {
  public:
   Tabla(int nElem);
   ~Tabla();
   T& operator[](int indice) { return pT[indice]; }
   int NElementos() const { return nElementos; }

  private:
   T *pT;
   int nElementos;
};

// Definición:
template <class T>
Tabla<T>::Tabla(int nElem) : nElementos(nElem) {
   pT = new T[nElementos];
}

template <class T>
Tabla<T>::~Tabla() {
   delete[] pT;
}
#endif

Fichero de aplicación de plantilla:

// Tabla.cpp: ejemplo de Tabla
// C con Clase: Marzo de 2002

#include <iostream>
#include "Tabla.h"

using namespace std;

const int nElementos = 10;

int main() {
   Tabla<int> TablaInt(nElementos);
   Tabla<float> TablaFloat(nElementos);

   for(int i = 0; i < nElementos; i++)
      TablaInt[i] = nElementos-i;

   for(int i = 0; i < nElementos; i++)
      TablaFloat[i] = 1.0f/(1.0f+i);

   for(int i = 0; i < nElementos; i++) {
      cout << "TablaInt[" << i << "] = "
           << TablaInt[i] << endl;
      cout << "TablaFloat[" << i << "] = "
           << TablaFloat[i] << endl;
   }

   return 0;
}

Posibles problemas

Ahora bien, supongamos que quieres usar la plantilla Tabla para crear una tabla de cadenas. Lo primero que se nos ocurre hacer probablemente sea:

Tabla<char*> TablaCad(15);

No hay nada que objetar, todo funciona, el programa compila, y no hay ningún error, pero... es probable que no funcione como esperas. Veamos otro ejemplo:

Fichero de aplicación de plantilla:

// Tablacad.cpp: ejemplo de Tabla con cadenas
// C con Clase: Marzo de 2002

#include <iostream>
#include <cstring>
#include <cstdio>
#include "Tabla.h"

using namespace std;

const int nElementos = 5;

int main() {
   Tabla<char *> TablaCad(nElementos);
   char cadena[20];

   for(int i = 0; i < nElementos; i++) {
      sprintf(cadena, "Numero: %5d", i);
      TablaCad[i] = cadena;
   }

   strcpy(cadena, "Modificada");

   for(int i = 0; i < nElementos; i++)
      cout << "TablaCad[" << i << "] = "
           << TablaCad[i] << endl;

   return 0;
}

Ejecutar este código en OnlineGDB.

Si has compilado el programa y has visto la salida, tal vez te sorprenda algo el resultado: Efectivamente, parece que nuestra tabla no es capaz de almacenar cadenas, o al menos no más de una cadena. La cosa sería aún más grave si la cadena auxiliar fuera liberada, por ejemplo porque se tratase de una variable local de una función, o porque se tratase de memoria dinámica.

TablaCad[0] = Modificada
TablaCad[1] = Modificada
TablaCad[2] = Modificada
TablaCad[3] = Modificada
TablaCad[4] = Modificada

¿Cuál es el problema?

Lo que pasa es que nuestra tabla no es de cadenas, sino de punteros a char. De hecho eso es lo que hemos escrito Tabla<char *>, por lo tanto, no hay nada sorprendente en el resultado. Pero esto nos plantea un problema: ¿cómo nos las apañamos para crear una tabla de cadenas?

Tablas de cadenas

La solución es usar una clase que encapsule las cadenas y crear una tabla de objetos de esa clase.

Veamos una clase básica para manejar cadenas:

// CCadena.h: Fichero de cabecera de definición de cadenas
// C con Clase: Marzo de 2002

#ifndef CCADENA
#define CCADENA
#include <cstring>

using std::strcpy;
using std::strlen;

class Cadena {
  public:
   Cadena(const char *cad) {
      cadena = new char[strlen(cad)+1];
      strcpy(cadena, cad);
   }
   Cadena() : cadena(NULL) {}
   Cadena(const Cadena &c) : cadena(NULL) {*this = c;}
   ~Cadena() { if(cadena) delete[] cadena; }
   Cadena &operator=(const Cadena &c) {
      if(this != &c) {
         if(cadena) delete[] cadena;
         if(c.cadena) {
            cadena = new char[strlen(c.cadena)+1];
            strcpy(cadena, c.cadena);
         }
         else cadena = NULL;
      }
      return *this;
   }
   const char* Lee() const {return cadena;}

  private:
   char *cadena;
};

std::ostream& operator<<(std::ostream &os, const Cadena& cad) {
   os << cad.Lee();
   return os;
}
#endif

Usando esta clase para cadenas podemos crear una tabla de cadenas usando nuestra plantilla:

// Tabla.cpp: ejemplo de Tabla de cadenas
// C con Clase: Marzo de 2002

#include <iostream>
#include <cstdio>
#include "Tabla.h"
#include "CCadena.h"

using namespace std;

const int nElementos = 5;

int main() {
   Tabla<Cadena> TablaCad(nElementos);
   char cadena[20];

   for(int i = 0; i < nElementos; i++) {
      sprintf(cadena, "Numero: %2d", i);
      TablaCad[i] = cadena;
   }

   strcpy(cadena, "Modificada");

   for(int i = 0; i < nElementos; i++)
      cout << "TablaCad[" << i << "] = "
           << TablaCad[i] << endl;

   return 0;
}

Ejecutar este código en codepad.

La salida de este programa es:

TablaCad[0] = Numero:  0
TablaCad[1] = Numero:  1
TablaCad[2] = Numero:  2
TablaCad[3] = Numero:  3
TablaCad[4] = Numero:  4

Ahora funciona como es debido.

El problema es parecido al que surgía con el constructor copia en clases que usaban memoria dinámica. El funcionamiento es correcto, pero el resultado no siempre es el esperado. Como norma general, cuando apliquemos plantillas, debemos usar clases con constructores sin parámetros, y tener cuidado cuando las apliquemos a tipos que impliquen punteros o memoria dinámica.

Funciones que usan plantillas como parámetros

Es posible crear funciones que admitan parámetros que sean una plantilla. Hay dos modos de pasar las plantillas: se puede pasar una instancia determinada de la plantilla o la plantilla genérica.

Pasar una instancia de una plantilla

Si declaramos y definimos una función que tome como parámetro una instancia concreta de una plantilla, esa función no estará disponible para el resto de las posibles instancias. Por ejemplo, si creamos una función para una tabla de enteros, esa función no podrá aplicarse a una tabla de caracteres:

// F_Tabla.cpp: ejemplo de función de plantilla para
// Tabla para enteros:
// C con Clase: Marzo de 2002

#include <iostream>
#include "Tabla.h"

using namespace std;

const int nElementos = 5;

void Incrementa(Tabla<int> &t); // (1)

int main() {
   Tabla<int> TablaInt(nElementos);
   Tabla<char> TablaChar(nElementos);

   for(int i = 0; i < nElementos; i++) {
      TablaInt[i] = 0;
      TablaChar[i] = 0;
   }

   Incrementa(TablaInt);
   // Incrementa(TablaChar); // <-- Ilegal (2)

   for(int i = 0; i < nElementos; i++)
      cout << "TablaInt[" << i << "] = "
           << TablaInt[i] << endl;

   return 0;
}

void Incrementa(Tabla<int> &t) { // (3)
   for(int i = 0; i < t.NElementos(); i++)
      t[i]++;
}

Ejecutar este código en codepad.

En (1) vemos que el argumento que especificamos es Tabla<int>, es decir, un tipo específico de plantilla: una instancia de int. Esto hace que sea imposible aplicar esta función a otros tipos de instancia, como en (2) para el caso de char, si intentamos compilar sin comentar esta línea el compilador dará error. Finalmente, en (3) vemos cómo se implementa la función, y cómo se usa el parámetro como si se tratase de una clase corriente.

Pasar una plantilla genérica

Si declaramos y definimos una función que tome como parámetro una instancia cualquiera, tendremos que crear una función de plantilla. Para ello, como ya hemos visto, hay que añadir a la declaración de la función la parte template<class T>.

Veamos un ejemplo:

// F_Tabla2.cpp: ejemplo de función de plantilla
// Tabla genérica:
// C con Clase: Marzo de 2002

#include <iostream>
#include <cstdio>
#include "Tabla.h"
#include "CCadena.h"

using namespace std;

const int nElementos = 5;

template<class T>
void Mostrar(Tabla<T> &t); // (1)

int main() {
   Tabla<int> TablaInt(nElementos);
   Tabla<Cadena> TablaCadena(nElementos);
   char cad[20];

   for(int i = 0; i < nElementos; i++) TablaInt[i] = i;
   for(int i = 0; i < nElementos; i++) {
      sprintf(cad, "Cad no.: %2d", i);
      TablaCadena[i] = cad;
   }

   Mostrar(TablaInt);    // (2)
   Mostrar(TablaCadena); // (3)

   return 0;
}

template<class T>
void Mostrar(Tabla<T> &t) { // (4)
   for(int i = 0; i < t.NElementos(); i++)
      cout << t[i] << endl;
}

Ejecutar este código en OnlineGDB.

En (1) vemos la forma de declarar una plantilla de función que puede aplicarse a nuestra plantilla de clase Tabla. En este caso, la función sí puede aplicarse a cualquier tipo de instancia de la clase Tabla, como se ve en (2) y (3). Finalmente, en (4) vemos la definición de la plantilla de función.

Amigos de plantillas

Por supuesto, en el caso de las plantillas también podemos definir relaciones de amistad con otras funciones o clases. Podemos distinguir dos tipos de funciones o clases amigas de plantillas de clases:

Clase o función amiga de una plantilla

Tomemos el caso de la plantilla de función que vimos en el apartado anterior. La función que pusimos como ejemplo no necesitaba ser amiga de la plantilla porque no era necesario que accediera a miembros privados de la plantilla. Pero podemos escribirla de modo que acceda directamente a los miembros privados, y para que ese acceso sea posible, debemos declarar la función como amiga de la plantilla.

Modificación de la plantilla Tabla con la función Mostrar como amiga de la plantilla:

// Tabla.h: definición de la plantilla tabla:
// C con Clase: Marzo de 2002

#ifndef T_TABLA
#define T_TABLA

template <class T> class Tabla; // Declaración adelantada de Tabla
template <class T> void Mostrar(Tabla<T> &t); // Para poder declarar la función Mostrar

template <class T>
class Tabla {
  public:
   Tabla(int nElem);
   ~Tabla();
   T& operator[](int indice) { return pT[indice]; }
   const int NElementos() {return nElementos;}

   friend void Mostrar<>(Tabla<T>&); // (1) Y poder declararla como amiga de Tabla

  private:
   T *pT;
   int nElementos;
};

// Definición:
template <class T>
Tabla<T>::Tabla(int nElem) : nElementos(nElem) {
   pT = new T[nElementos];
}

template <class T>
Tabla<T>::~Tabla() {
   delete[] pT;
}
#endif

Programa de ejemplo:

// F_Tabla2.cpp: ejemplo de función amiga de
// plantilla Tabla genérica:
// C con Clase: Marzo de 2002

#include <iostream>
#include <cstdio>
#include "Tabla.h"
#include "CCadena.h"

using namespace std;

const int nElementos = 5;

template<class T>
void Mostrar(Tabla<T> &t);  // (2)

int main() {
   Tabla<int> TablaInt(nElementos);
   Tabla<Cadena> TablaCadena(nElementos);
   char cad[20];

   for(int i = 0; i < nElementos; i++) TablaInt[i] = i;
   for(int i = 0; i < nElementos; i++) {
      sprintf(cad, "Cad no.: %2d", i);
      TablaCadena[i] = cad;
   }

   Mostrar(TablaInt);
   Mostrar(TablaCadena);

   return 0;
}

template<class T>
void Mostrar(Tabla<T> &t) { // (3)
   for(int i = 0; i < t.nElementos; i++)
      cout << t.pT[i] << endl;
}

Ejecutar este código en OnlineGDB.

Nota (1):

aunque esto no sea del todo estándar, algunos compiladores, (sin ir más lejos los que usa Dev-C++), requieren que se incluya <> a continuación del nombre de la función que declaramos como amiga, cuando esa función sea una plantilla de función. Si te fijas en (2) y (3) verás que eso no es necesario cuando se declara el prototipo o se define la plantilla de función. En otros compiladores puede que no sea necesario incluir <>, por ejemplo, en Borland C++ no lo es.

Clase o función amiga de una instancia de una plantilla

Limitando aún más las relaciones de amistad, podemos declarar funciones amigas de una única instancia de la plantilla.

Dentro de la declaración de una plantilla podemos declarar una clase o función amiga de una determinada instancia de la plantilla, la relación de amistad no se establece para otras posibles instancias de la plantilla. Usando el mismo último ejemplo, pero sustituyendo la línea de la declaración de la clase que hace referencia a la función amiga:

template<class T>
class Tabla {
   ...
   friend void Mostrar<>(Tabla<int>&);
   ...
};

Esto hace que sólo se pueda aplicar la función Mostrar a las instancias <int> de la plantilla Tabla. Por supuesto, tanto el prototipo como la definición de la función "Mostrar" no cambian en nada, debemos seguir usando una plantilla de función idéntica que en el ejemplo anterior.

Miembros estáticos: datos y funciones

Igual que con las clases normales, es posible declarar datos miembro o funciones estáticas dentro de una plantilla. En este caso existirá una copia de cada uno de ellos para cada tipo de instancia que se cree.

Por ejemplo, si añadimos un miembro static en nuestra declaración de "Tabla", se creará una única instancia de miembro estático para todas las instancias de Tabla<int>, otro para las instancias de Tabla<Cadena>, etc.

Hay un punto importante a tener en cuenta. Tanto si trabajamos con clases normales como con plantillas, debemos reservar un espacio físico para las variables estáticas, de otro modo el compilador no les asignará memoria, y se producirá un error si intentamos acceder a esos miembros.

Nota:

Tanto una plantilla como una clase pueden estar definidas en un fichero de cabecera (.h), aunque es más frecuente con las plantillas, ya que toda su definición debe estar en el fichero de cabecera; bien dentro de la declaración, en forma de funciones inline o bien en el mismo fichero de cabecera, a continuación de la declaración de la plantilla. Estos ficheros de cabecera pueden estar incluidos en varios módulos diferentes de la misma aplicación, de modo que el espacio físico de los miembros estáticos debe crearse sólo una vez en la memoria global de la aplicación, y por lo tanto, no debe hacerse dentro de los ficheros de cabecera.

Veamos un ejemplo:

// Fichero de cabecera: prueba.h
#ifndef T_PRUEBA
#define T_PRUEBA

template <class T>
class Ejemplo {
   public:
    Ejemplo(T obj) {objeto = obj; estatico++;}
    ~Ejemplo() {estatico--;}
    static int LeeEstatico() {return estatico;}

   private:
    static int estatico; // (1)
    T objeto; // Justificamos el uso de la plantilla :-)
};

#endif

Ahora probemos los miembros estáticos de nuestra clase:

// Fichero de prueba: prueba.cpp
#include <iostream>
#include "prueba.h"

using namespace std;

// Esto es necesario para que exista
// una instancia de la variable:
template <class T> int Ejemplo<T>::estatico;   // (2)

int main() {
   Ejemplo<int> EjemploInt1(10);
   cout << "Ejemplo<int>: " << EjemploInt1.LeeEstatico()
        << endl;  // (3)
   Ejemplo<char> EjemploChar1('g');
   cout << "Ejemplo<char>: "
        << EjemploChar1.LeeEstatico() << endl;  // (4)
   Ejemplo<int> EjemploInt2(20);
   cout << "Ejemplo<int>: "
        << EjemploInt1.LeeEstatico() << endl;  // (5)
   Ejemplo<float> EjemploFloat1(32.12);
   cout << "Ejemplo<float>: "
        << Ejemplo<float>::LeeEstatico() << endl;  // (6)
   Ejemplo<int> EjemploInt3(30);
   cout << "Ejemplo<int>: "
        << EjemploInt1.LeeEstatico() << endl;  // (7)

   return 0;
}

Ejecutar este código en OnlineGDB.

La salida de este programa debería ser algo así:

Ejemplo<int>: 1
Ejemplo<char>: 1
Ejemplo<int>: 2
Ejemplo<float>: 1
Ejemplo<int>: 3

Vamos a ver si explicamos algunos detalles de este programa.

Para empezar, en (1) vemos la declaración de la variable estática de la plantilla y en (2) la definición. En realidad se trata de una plantilla de declaración, en cierto modo, ya que adjudica memoria para cada miembro estático de cada tipo de instancia de la plantilla. Prueba a ver qué pasa si no incluyes esta línea.

El resto de los puntos muestra que realmente se crea un miembro estático para cada tipo de instancia. En (2) se crea una instancia de tipo Ejemplo<int>, en (3) una de tipo Ejemplo<char>, en (4) una segunda de tipo Ejemplo<int>, etc. Eso de ve también en los valores de salida.

Ejemplo de implementación de una plantilla para una pila

Considero que el tema es lo bastante interesante como para incluir algún ejemplo ilustrativo. Vamos a crear una plantilla para declarar pilas de cualquier tipo de objeto, y de ese modo demostraremos parte de la potencia de las plantillas.

Para empezar, vamos a ver la declaración de la plantilla, como siempre, incluida en un fichero de cabecera para ella sola:

// Pila.h: definición de plantilla para pilas
// C con Clase: Marzo de 2002

#ifndef TPILA
#define TPILA

// Plantilla para pilas genéricas:
template <class T>
class Pila {
   // Plantilla para nodos de pila, definición
   // local, sólo accesible para Pila:
   template <class Tn>
   class Nodo {
     public:
      Nodo(const Tn& t, Nodo<Tn> *ant) : anterior(ant) {
         pT = new Tn(t);
      }
      Nodo(Nodo<Tn> &n) { // Constructor copia
         // Invocamos al constructor copia de la clase de Tn
         pT = new Tn(*n.pT);
         anterior = n.anterior;
      }
      ~Nodo() { delete pT; }
      Tn *pT;
      Nodo<Tn> *anterior;
   };
   ///// Fin de declaración de plantilla de nodo /////

  // Declaraciones de Pila:
  public:
   Pila() : inicio(NULL) {} // Constructor
   ~Pila() { while(inicio) Pop(); }
   void Push(const T &t) {
      Nodo<T> *aux = new Nodo<T>(t, inicio);
      inicio = aux;
   }
   T Pop() {
      T temp(*inicio->pT);
      Nodo<T> *aux = inicio;
      inicio = aux->anterior;
      delete aux;
      return temp;
   }
   bool Vacia() { return inicio == NULL; }

  private:
   Nodo<T> *inicio;
};

#endif

Aquí hemos añadido una pequeña complicación: hemos definido una plantilla para Nodo dentro de la plantilla de Pila. De este modo definiremos instancias de Nodo locales para cada instancia de Pila. Aunque no sea necesario, ya que podríamos haber creado dos plantillas independientes, hacerlo de este modo nos permite declarar ambas clases sin necesidad de establecer relaciones de amistad entre ellas. De todos modos, el nodo que hemos creado para la estructura Pila no tiene uso para otras clases.

En cuanto a la definición de la Pila, sólo hemos declarado cinco funciones: el constructor, el destructor, las funciones Push, para almacenar objetos en la pila y Pop para recuperarlos y Vacia para comprobar si la pila está vacía.

En cuanto al constructor, sencillamente construye una pila vacía.

El destructor recupera todos los objetos almacenados en la pila. Recuerda que en las pilas, leer un valor implica borrarlo o retirarlo de ella.

La función Push coloca un objeto en la pila. Para ello crea un nuevo nodo que contiene una copia de ese objeto y hace que su puntero "anterior" apunte al nodo que hasta ahora era el último. Después actualiza el inicio de la Pila para que apunte a ese nuevo nodo.

La función Pop recupera un objeto de la pila. Para ello creamos una copia temporal del objeto almacenado en el último nodo de la pila, esto es necesario porque lo siguiente que hacemos es actualizar el nodo de inicio (que será el anterior al último) y después borrar el último nodo. Finalmente devolvemos el objeto temporal.

Ahora vamos a probar nuestra pila, para ello haremos un pequeño programa:

// Pru_Pila.cpp: Prueba de plantilla pila
// C con Clase: Marzo de 2002

#include <iostream>
#include "pila.h"
#include "CCadena.h"

using namespace std;

// Ejemplo de plantilla de función:
template <class T>
void Intercambia(T &x, T &y) {
   Pila<T> pila;

   pila.Push(x);
   pila.Push(y);
   x = pila.Pop();
   y = pila.Pop();
}

int main() {
   Pila<int> PilaInt;
   Pila<Cadena> PilaCad;

   int x=13, y=21;
   Cadena cadx("Cadena X");
   Cadena cady("Cadena Y ----");

   cout << "x=" << x << endl;
   cout << "y=" << y << endl;
   Intercambia(x, y);
   cout << "x=" << x << endl;
   cout << "y=" << y << endl;

   cout << "cadx=" << cadx << endl;
   cout << "cady=" << cady << endl;
   Intercambia(cadx, cady);
   cout << "cadx=" << cadx << endl;
   cout << "cady=" << cady << endl;

   PilaInt.Push(32);
   PilaInt.Push(4);
   PilaInt.Push(23);
   PilaInt.Push(12);
   PilaInt.Push(64);
   PilaInt.Push(31);

   PilaCad.Push("uno");
   PilaCad.Push("dos");
   PilaCad.Push("tres");
   PilaCad.Push("cuatro");

   cout << PilaInt.Pop() << endl;
   cout << PilaInt.Pop() << endl;
   cout << PilaInt.Pop() << endl;
   cout << PilaInt.Pop() << endl;
   cout << PilaInt.Pop() << endl;
   cout << PilaInt.Pop() << endl;

   cout << PilaCad.Pop() << endl;
   cout << PilaCad.Pop() << endl;
   cout << PilaCad.Pop() << endl;
   cout << PilaCad.Pop() << endl;

   return 0;
}

Ejecutar este código en OnlineGDB.

La salida demuestra que todo funciona:

x=13
y=21
x=21
y=13
cadx=Cadena X
cady=Cadena Y ----
cadx=Cadena Y ----
cady=Cadena X
31
64
12
23
4
32
cuatro
tres
dos
uno

Hemos aprovechado para crear una plantilla de función para intercambiar valores de objetos, que a su vez se basa en la plantilla de pila, y aunque esto no sería necesario, reconocerás que queda bonito :-).

Bibliotecas de plantillas

El ANSI de C++ define ciertas bibliotecas de plantillas conocidas como STL (Standard Template Library), que contiene muchas definiciones de plantillas para crear estructuras como listas, colas, pilas, árboles, tablas HASH, mapas, etc.

Está fuera del objetivo de este curso explicar estas bibliotecas, pero es conveniente saber que existen y que por su puesto, se suelen incluir con los compiladores de C++.

Aunque se trata de bibliotecas estándar, lo cierto es que existen varias implementaciones, que, al menos en teoría, deberían coincidir en cuanto a sintaxis y comportamiento.

Palabra typename

template <typename T> class Plantilla;

Es equivalente usar typename y class como parte de la declaración de T, en ambos casos T puede ser una clase o un tipo fundamental, como int, char o float. Sin embargo, usar typename puede ser mucho más claro como nombre genérico que class.

Palabras reservadas usadas en este capítulo

template, typename.