Introducción a las Plantillas de Función
Las plantillas en C++ son el pilar fundamental de la programación genérica. Permiten diseñar algoritmos y estructuras que operan con diversos tipos de datos sin duplicar código. Durante la fase de compilación, el compilador deduce los tipos y genera las versiones específicas necesarias, un mecanismo conocido como instanciación.
Sintaxis Básica
Para declarar una plantilla de función, se emplea la palabra clave template seguida de los parámetros de tipo entre corchetes angulares:
template <typename T>
void intercambiar(T& valorA, T& valorB) {
T temporal = valorA;
valorA = valorB;
valorB = temporal;
}
Mecanismo de Instanciación
La instanciación se produce cuando el compilador resuelve los tipos de datos para construir la función concreta. Existen dos enfoques principales:
- Instanciación Implícita: El compilador infiere el tipo analizando los argumentos proporcionados en la llamada. Es vital que los tipos coincidan o se apliquen conversiones explícitas si la deducción automática falla.
- Instanciación Explícita: El programador especifica directamente el tipo entre corchetes angulares al invocar la función.
template <class T>
T sumar(const T& num1, const T& num2) {
return num1 + num2;
}
int main() {
int enteroA = 15, enteroB = 25;
double decimalA = 15.5, decimalB = 25.5;
// Instanciación implícita
sumar(enteroA, enteroB);
sumar(decimalA, decimalB);
// Instanciación explícita
sumar<int>(enteroA, static_cast<int>(decimalA));
return 0;
}
Resolución de Sobrecarga y Coincidencia
Cuando coexisten funciones convencionales y plantillas con el mismo identificador, el compilador aplica reglas estrictas de resolución:
- Una función no genérica tiene prioridad sobre una plantilla si los argumentos coinciden de manera exacta.
- Si la plantilla puede generar una versión que se ajuste mejor que la función normal (por ejemplo, aceptando tipos mixtos), el compilador seleccionará la plantilla.
- Las plantillas no ejecutan conversiones de tipo automáticas en sus parámetros deducidos, a diferencia de las funciones normales.
Plantillas de Clase
Las plantillas también son aplicables a clases, facilitando la creación de estructuras de datos genéricas. A diferencia de las funciones, las plnatillas de clase siempre exigen instanciación explícita al declarar objetos.
template <typename T>
class ContenedorDinamico {
public:
ContenedorDinamico(size_t capacidadInicial = 8)
: capacidad(capacidadInicial), total(0) {
datos = new T[capacidad];
}
~ContenedorDinamico() {
delete[] datos;
}
void insertar(const T& elemento) {
if (total == capacidad) {
size_t nuevaCapacidad = capacidad * 2;
T* nuevoBloque = new T[nuevaCapacidad];
for(size_t i = 0; i < total; ++i) {
nuevoBloque[i] = datos[i];
}
delete[] datos;
datos = nuevoBloque;
capacidad = nuevaCapacidad;
}
datos[total++] = elemento;
}
private:
T* datos;
size_t capacidad;
size_t total;
};
int main() {
ContenedorDinamico<int> pilaEnteros;
pilaEnteros.insertar(100);
ContenedorDinamico<double> pilaDecimales;
pilaDecimales.insertar(3.1415);
return 0;
}
Compilación Separada
Un error frecuente es dividir la declaración y la definición de una plantilla en archivos .h y .cpp. Esto genera errores de enlazador porque el compilador no puede instanciar el código en la unidad de traducción donde se consume. La solución estándar es mantener ambas en el mismo archivo (usualmente con extensión .hpp o .h) o forzar la instanciación explícita en el archivo .cpp, aunque esta última alternativa es rígida y poco escalable.
Parámetros de Plantilla No Tipados
Además de tipos, las plantillas pueden aceptar valores constantes evaluables en tiempo de compilación, conocidos como parámetros no tipados. Estos resultan útiles para establecer tamaños de arreglos o límites estáticos.
template <typename T, size_t LIMITE = 50>
class BufferFijo {
public:
T& operator[](size_t indice) { return memoria[indice]; }
const T& operator[](size_t indice) const { return memoria[indice]; }
size_t obtenerTamano() const { return LIMITE; }
private:
T memoria[LIMITE];
};
int main() {
BufferFijo<int, 100> bufferGrande;
BufferFijo<char> bufferPorDefecto; // Emplea el límite por defecto de 50
return 0;
}
Los parámetros no tipados deben resolverse durante la compilación. Históricamente, solo se admitían tipos integrales y punteros, pero a partir de C++20, las restricciones se han flexibilizado para incluir tipos de punto flotante y ciertas clases literales.
Especialización de Plantillas
En determinados escenarios, la implementación genérica no es óptima para ciertos tipos de datos, como punteros o clases con comportamientos particulares. La especialización permite suministrar una implementación alternativa para estos casos específicos.
Especialización de Funciones
Para especializar una función, primero debe existir la plantilla base. Posteriormente, se define la versión específica utilizando template<> y detallando el tipo.
class Evento {
public:
Evento(int a = 2000, int m = 1, int d = 1)
: anio(a), mes(m), dia(d) {}
bool operator<(const Evento& otro) const {
if (anio != otro.anio) return anio < otro.anio;
if (mes != otro.mes) return mes < otro.mes;
return dia < otro.dia;
}
private:
int anio, mes, dia;
};
// Plantilla base
template <class T>
bool esMenor(const T& izq, const T& der) {
return izq < der;
}
// Especialización para punteros a Evento
template <>
bool esMenor<Evento*>(Evento* const& izq, Evento* const& der) {
return *izq < *der;
}
En la práctica moderna, en lugar de especializar funciones, suele ser preferible crear sobrecargas de funciones normales, ya que el mecanismo de resolución de sobrecargas de C++ gestiona las funciones no genéricas de manera más predecible.
Especialización de Clases
Las clases pueden especializarse de dos formas: total o parcialmente.
Especialización Total: Se fijan todos los parámetros de la plantilla.
template <class T1, class T2>
class Registro {
public:
Registro() { /* Implementación genérica */ }
};
template <>
class Registro<int, char> {
public:
Registro() { /* Implementación específica para int y char */ }
};
Especialización Parcial: Se restringen algunos parámetros a ciertas categorías (como punteros o referencias) o se fijan parcialmente, manteniendo otros genéricos.
// Parcial: El segundo tipo es siempre double
template <class T1>
class Registro<T1, double> {
public:
Registro() { /* Lógica para cuando T2 es double */ }
};
// Parcial: Ambos tipos son punteros
template <typename T1, typename T2>
class Registro<T1*, T2*> {
public:
Registro() { /* Lógica para punteros */ }
};
Al especializar una clase, todos sus métodos deben ser redefinidos en la especialización, ya que el compilador la trata como una entidad completamente nueva e independiente de la plantilla original.