La palabra reservada template le dice al compilador que la definición de clase que sigue manipulará uno o más tipos no especificados. En el momento en que se genera el código real de la clase a partir de la plantilla, hay que especificar esos tipos para que el compilador pueda sustituirlos.
Para demostrar la sintaxis, aquí está un pequeño ejemplo que produce un array con límites comprobados:
//: C16:Array.cpp #include "../require.h" #include <iostream> using namespace std; template<class T> class Array { enum { size = 100 }; T A[size]; public: T& operator[](int index) { require(index >= 0 && index < size, "Index out of range"); return A[index]; } }; int main() { Array<int> ia; Array<float> fa; for(int i = 0; i < 20; i++) { ia[i] = i * i; fa[i] = float(i) * 1.414; } for(int j = 0; j < 20; j++) cout << j << ": " << ia[j] << ", " << fa[j] << endl; } ///:~
Listado 16.4. C16/Array.cpp
Se puede ver que parece una clase normal excepto por la línea
template<class T>
que indica que T es un parámetro de sustitución, y que representa un nombre de tipo. Además, se puede ver que T se usa en todas las partes de la clase donde normalmente se vería el tipo específico que el contenedor gestiona.
En Array los elementos son insertados y extraídos con la misma función: el operador sobrecargado operator[]. Devuelve una referencia, por lo que puede ser usado en ambos lados del signo igual (es decir, tanto como valor izquierdo como valor derecho). Hay que hacer notar que si el índice se sale de los límites se usa la función require() para mostrar un mensaje. Como operator[] es inline, se puede usar esta aproximación para garantizar que no se producen violaciones de los límites del array, y después eliminar el require() en el código que se entregue.
En el main(), se puede ver lo fácil que es crear Arrays que manejen distintos tipos de objetos. Cuando se dice:
Array<int> ia; Array<float> fa;
el compilador expande dos veces la plantilla Array (esto se conoce como instanciación), para crear dos nuevas clases generadas, que se pueden imaginar como Array_int y Array_float. (Diferentes compiladores pueden decorar los nombres de diferentes maneras). Estas clases son idénticas a las que se habrían obtenido haciendo la sustitución a mano, excepto que el compilador las crea por nosotros cuando se definen los objetos ia y fa. También hay que notar que las definiciones de clases duplicadas o bien las evita el compilador o bien las fusiona el enlazador.
Por supuesto, hay veces en las que se querrá tener definiciones de funciones miembro no inline. En ese caso, el compilador necesita ver la declaración de la plantilla antes que la definición de la función miembro. Aquí está el ejemplo anterior, modificado para mostrar la definición del miembro no inline:
//: C16:Array2.cpp // Non-inline template definition #include "../require.h" template<class T> class Array { enum { size = 100 }; T A[size]; public: T& operator[](int index); }; template<class T> T& Array<T>::operator[](int index) { require(index >= 0 && index < size, "Index out of range"); return A[index]; } int main() { Array<float> fa; fa[0] = 1.414; } ///:~
Listado 16.5. C16/Array2.cpp
Cualquier referencia al nombre de una plantilla de clase debe estar acompañada por su lista de argumentos de plantilla, como en Array<T>::operator[]. Se puede imaginar que internamente, el nombre de la clase se decora con los argumentos de la lista de argumentos de la plantilla para producir un nombre identificador único de la clase por cada instanciación de la plantilla.
Incluso si se crean definiciones de funciones no inline, normalmente se querrá poner todas las declaraciones y definiciones de una plantilla en un fichero de cabecera. Esto parece violar la regla usual de los ficheros de cabecera de «No poner nada que asigne almacenamiento», (lo cual previene errores de definición múltiple en tiempo de enlace), pero las definiciones de plantillas son especiales. Todo lo que vaya precedido por template<...> significa que el compilador no asignará almacenamiento en ese momento, sino que esperará hasta que se lo indiquen (en la instanciación de una plantilla), y que en algún lugar del compilador y del enlazador hay un mecanismo para eliminar las múltiples definiciones de una plantilla idéntica. Por lo tanto casi siempre se pondrá toda la declaración y definición de la plantilla en el fichero de cabecera por facilidad de uso.
Hay veces en las que puede ser necesario poner las definiciones de la plantilla en un fichero cpp separado para satisfacer necesidades especiales (por ejemplo, forzar las instanciaciones de las plantillas para que se encuentren en un único fichero dll de Windows). La mayoría de los compiladores tienen algún mecanismo para permitir esto; hay que investigar la documentación del compilador concreto para usarlo.
Algunas personas opinan que poner todo el código fuente de la implementación en un fichero de cabecera hace posible que otros roben y modifiquen el código si le compran a usted la librería. Esto puede ser un inconveniente, pero probablemente dependa del modo de mirar el problema: ¿Se está comprando un producto o un servicio? Si es un producto, entonces hay que hacer todo lo posible por protegerlo, y probablemente no se quiera dar el código fuente, sino sólo el código compilado. Pero mucha gente ve el software como un servicio, incluso más, como un servicio por suscripción. El cliente quiere la pericia de usted, quiere que siga manteniendo ese fragmento de código reutilizable para no tener que hacerlo él, y así poder centrarse en hacer su propio trabajo. Personalmente creo que la mayoría de los clientes le tratarán como un recurso valioso y no querrán poner en peligro su relación con usted. Y en cuanto a los pocos que quieran robar en vez de comprar o hacer el trabajo original, de todas formas probablemente no podrían seguirle el ritmo.