16.3. Sintaxis del Template

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

16.3.1. Definiciones de función no inline

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.

Archivos cabecera

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.