Las plantillas en C++ representan una herramienta fundamental para la programación genérica, permitiendo a los desarrolladores escribir código que funciona con diversos tipos de datos sin tener que duplicar la lógica para cada tipo. Una plantilla de función es esencialmente un esquema o una fórmula para crear funciones, donde el tipo o los tipos de datos sobre los que opera la función se dejan como parámetros.
Cuando el compilador encuentra una llamada a una función plantilla, genera una versión específica de la función para el tipo de datos utilizado en esa llamada. Este proceso se conoce como instanciación. La función concreta resultante se denomina función plantilla.
- Parámetros de Tipo de Plantilla: Son los marcadores de posición para los tipos de datos, comúnmente declarados con
typename Toclass T. - Deducción de Argumentos de Plantilla: El compilador puede inferir automáticamente los tipos de los argumentos de plantilla basándose en los tipos de los parámetros pasados durante la llamada a la función.
- Especialización de Plantilla: Permite definir un comportamiento personalizado para una función plantilla cuando se utiliza con tipos específicos. Esto es crucial cuando la implementación genérica no es adecuada o eficiente para ciertos tipos.
- Parámetros de Plantilla No de Tipo: Además de los tipos, las plantillas pueden aceptar valores constantes (como enteros, punteros o referencias) como parámetros, que se conocen en tiempo de compilación.
Comportamiento de las Plantillas de Función y su Especialización
Consideremos un ejemplo de una función plantilla que compara dos valores. Para la mayoría de los tipos numéricos, la comparación directa es suficiente. Sin embargo, para cadenas de caracteres (representadas como const char*), necesitamos una lógica de comparación lexicográfica en lugar de comparar direcciones de memoria.
#include <iostream>
#include <cstring> // Para strcmp
// Plantilla de función principal para comparar si el primer valor es mayor que el segundo
template<typename Tipo>
bool esMayorQue(Tipo valor1, Tipo valor2) {
std::cout << "Usando plantilla generica" << std::endl;
return valor1 > valor2;
}
// Especialización de plantilla para comparar cadenas de caracteres (const char*)
// Esto proporciona un comportamiento específico para las cadenas.
template<>
bool esMayorQue<const char*>(const char* cadena1, const char* cadena2) {
std::cout << "Usando especializacion para cadenas" << std::endl;
return std::strcmp(cadena1, cadena2) > 0;
}
// Opcional: Una función no plantilla (normal) que sobrecarga el nombre.
// Si existe una sobrecarga no plantilla que coincide, tiene la máxima prioridad.
bool esMayorQue(int a, int b) {
std::cout << "Usando funcion no plantilla para enteros" << std::endl;
return a > b;
}
int main() {
// Deducción de tipo y uso de la plantilla genérica
std::cout << "10 > 5? " << esMayorQue(10, 5) << std::endl; // Si no hubiera version no plantilla, deduciría int y usaría la genérica.
std::cout << "3.14 > 2.71? " << esMayorQue(3.14, 2.71) << std::endl; // Deduce double, usa la genérica
// Instanciación explícita (aunque la deducción funcionaría aquí)
std::cout << "25 > 15 (explicit int)? " << esMayorQue<int>(25, 15) << std::endl; // Fuerza la instanciación de la plantilla genérica para int
// Uso de la especialización de plantilla para const char*
std::cout << "\"hola\" > \"adios\"? " << esMayorQue("hola", "adios") << std::endl; // Deduce const char*, usa la especialización
// Demostración de la prioridad: la función no plantilla para int gana sobre la plantilla genérica para int.
// La llamada esMayorQue(100,50) invocará la función `bool esMayorQue(int a, int b)`
std::cout << "Llamando con enteros que activan la no plantilla: " << esMayorQue(100, 50) << std::endl;
return 0;
}
En el ejemplo anterior, la llamada esMayorQue("hola", "adios") invoca la especialización para const char*, ya que el comportamiento predeterminado de comparar direcciones de memoria sería incorrecto para el significado deseado de "mayor que" en cadenas.
Es importante entender la jerarquía de resolución de sobrecargas:
- Funciones no plantilla (normales) con una coincidencia exacta.
- Especializaciones de plantilla.
- Plantillas de función genéricas.
Esto significa que si existe una función no plantilla que coincide perfectamente con los argumentos, esta se seleccionará antes que cualquier plantilla o especialización. ### Consideraciones sobre la Compilación Separada
Un error común al trabajar con plantillas es intentar separar la declaración de una plantilla (en un archivo .h) de su definición (en un archivo .cpp). Las plantillas no son funciones compiladas en sí mismas, sino instrucciones para el compilador sobre cómo generar funciones. Por lo tanto, el código fuente completo de la plantilla (tanto declaración como definición) debe estar disponible para el compilador en el punto de instanciación. La solución estándar es incluir la definición completa de la plantilla directamente en el archivo de cabecera.
Parámetros de Plantilla No de Tipo
Además de los tipos, las plantillas pueden tomar parámetros que no son tipos. Estos son valores que deben ser constantes en tiempo de compilación. Son útiles para incrustar información como tamaños de arrays o valores de configuración directamente en el tipo de la plantilla.
#include <iostream>
#include <algorithm> // Para std::swap, si se desea una implementación más robusta
// Plantilla de función para ordenar un arreglo, donde el tamaño es un parámetro no de tipo
template<typename TipoElemento, int Tamano>
void ordenarArreglo(TipoElemento (&arr)[Tamano]) { // El parámetro es una referencia a un array de tamaño fijo
std::cout << "Ordenando un arreglo de " << Tamano << " elementos." << std::endl;
// Implementación simple de Bubble Sort
for (int i = 0; i < Tamano - 1; ++i) {
for (int j = 0; j < Tamano - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
// Intercambio de elementos
TipoElemento temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
// Alternativa: std::swap(arr[j], arr[j + 1]);
}
}
}
}
// Función auxiliar para imprimir el arreglo
template<typename TipoElemento, int Tamano>
void imprimirArreglo(TipoElemento (&arr)[Tamano]) {
std::cout << "Arreglo: [";
for (int i = 0; i < Tamano; ++i) {
std::cout << arr[i];
if (i < Tamano - 1) {
std::cout << ", ";
}
}
std::cout << "]" << std::endl;
}
int main() {
int numeros[] = {5, 2, 9, 1, 7};
imprimirArreglo(numeros);
ordenarArreglo(numeros); // El compilador deduce TipoElemento como int y Tamano como 5
imprimirArreglo(numeros);
char letras[] = {'z', 'x', 'a', 'c'};
imprimirArreglo(letras);
ordenarArreglo(letras); // Deduce TipoElemento como char y Tamano como 4
imprimirArreglo(letras);
return 0;
}
En este ejemplo de ordenarArreglo, Tamano es un parámetro de plantilla no de tipo que se determina en tiempo de compilación a partir del tamaño del arreglo pasado. Esto permite que la función de ordenación trabaje con arreglos de diferentes tamaños de forma segura y eficiente, ya que el tamaño es conocido y fijo en el momento de la instanciación de la plantilla.