La palabra reservada template
le dice al compilador que la definición de clases que sigue
manipulará uno o más tipos no especificados. En el momento en
que el código de la clase actual es generado, los tipos deben
ser especificados 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 un tipo. Además, se
puede ver que T
es usado en todas las
partes de la clase donde normalmente se vería al tipo específico
que el contenedor gestiona.
En Array
los elementos son insertados y
extraidos 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 lvalue
como
rvalue
). 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 del límite del
array para entonces eliminar el require()
.
En el main()
, se puede ver lo fácil que es
crear Array
s que manejen distintos tipos de
objetos. Cuando se dice:
Array<int> ia; Array<float> fa;
el compilador expande dos veces la plantilla del
Array
(que se conoce como instantiation
o crear una instancia),
para crear dos nuevas clases generadas, las
cuales pueden ser interpretadas como Array_int
y Array_float
. Diferentes
compiladores pueden crear los nombres de diferentes
maneras. Estas clases son idénticas a las que hubieran producido
de estar hechas 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 son eludidas por el
compilador.
Por supuesto, hay veces en las que se querrá tener definición
de funciones no inline. En ese caso, el compilador necesita
ver la declaración del template
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ñado por la lista de argumentos del template, como
en
Array<T>operator[]
. Se
puede imaginar que internamente, el nombre de la clase se
rellena con los argumentos de la lista de argumentos de la
plantilla para producir un nombre identificador único de la
clase for 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 un template en un archivo cabecera. Esto
parece violar la regla usual de los archivos cabecera de
«No poner nada que asigne almacenamiento», (lo
cual previene múltiples errores de definición en tiempo de
enlace), pero las definiciones de plantillas son
especial. Algo precedido por
template<...>
significa que el compilador no asignará almacenamiento en
ese momento, sino que se 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 archivo
cabecera por facilidad de uso.
Hay veces en las que puede ser necesario poner las
definiciones de la plantilla en un archivo cpp
separado para satisfacer
necesidades especiales (por ejemplo, forzar las
instanciaciones de las plantillas para que se encuentren en
un único archivo 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 sienten que poner el código fuente de la implementación en un archivo cabecera hace posible que se pueda robar y modificar el código si se compra la librería. Esto puede ser una característica, 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 nuestra pericia, quieren que se mantenga ese fragmento de código reutilizable para no tenerlo que hacer él - para que se pueda enfocar en hacer su propio trabajo. Personalmente creo que la mayoría de los clientes le tratarán como una fuente de recursos a tener en cuenta y no querrán poner en peligro su relación con usted. Y para los pocos que quieran robar en vez de comprar o hacer el trabajo original, de todas formas probablemante tampoco se mantendrían con usted.