Genéricos y tipificación en C++: ¿por qué no son «simples plantillas»?
Cuando un desarrollador pasa de Python o Java a C++, a menudo le asusta la palabra «plantilla» (template). En su mente surgen asociaciones con interminables corchetes angulares, errores de compilación incomprensibles y la magia de std::vector<int>. En realidad, los genéricos en C++ (o programación genérica) son una herramienta potentísima que permite escribir código una vez y usarlo con cualquier tipo de dato. Y sí, no es lo mismo que los genéricos en Java o C#.
En este artículo analizaremos cómo funciona la tipificación en C++ a través de plantillas, en qué se diferencian las plantillas de clases de las plantillas de funciones, y por qué se añadieron los conceptos en C++20. Habrá muchos ejemplos, desde simples hasta aquellos que realmente serán útiles en producción. ¡Allá vamos!
¿Qué son los genéricos (plantillas) en C++?
De forma muy simple: las plantillas en C++ son una forma de escribir código que funciona con cualquier tipo de dato sin perder rendimiento. A diferencia de la tipificación dinámica de Python, donde el tipo se verifica en tiempo de ejecución, C++ genera una versión separada de la función o clase para cada tipo utilizado en tiempo de compilación. Esto se llama tipificación estática con parametrización.
Sintácticamente, una plantilla se declara con la palabra clave template y corchetes angulares con parámetros:
template <typename T>T max(T a, T b) { return (a > b) ? a : b;}Aquí typename T es el parámetro de la plantilla. Puede usar class en lugar de typename; no hay diferencia. Cuando escribe max(3, 7), el compilador sustituye automáticamente int por T y crea la función int max(int, int). Si escribe max(3.14, 2.71), obtendrá la versión para double. Simple y eficiente.
Plantillas de funciones: el primer paso hacia los genéricos
Empecemos por lo básico: las plantillas de funciones en C++. Son la forma ideal de evitar la duplicación de código cuando la lógica es la misma pero los tipos son diferentes.
Ejemplo: una función que imprime un elemento en la consola (útil para depuración):
#include <iostream>#include <string>
template <typename T>void print(const T& value) { std::cout << value << std::endl;}
int main() { print(42); // int print(3.1415); // double print("Hello"); // const char* print(std::string("¡Genéricos!")); // std::string return 0;}Observe: usamos const T& — esto es tipificación en C++ con paso por referencia constante, para evitar copiar objetos grandes. Para tipos nativos (int, double) no es crítico, pero es un buen hábito.
¿Y si necesitamos pasar dos argumentos de diferentes tipos? Fácil:
template <typename T1, typename T2>auto add(T1 a, T2 b) -> decltype(a + b) { return a + b;}
// En C++14 y posteriores se puede simplemente:template <typename T1, typename T2>auto add(T1 a, T2 b) { return a + b;}Aquí auto deduce el tipo de retorno automáticamente. Esto es los genéricos en acción: un código, infinitas combinaciones de tipos.
Plantillas de clases: construyendo estructuras universales
Ahora pasemos a las plantillas de clases en C++. Son la base de la biblioteca estándar (STL): std::vector<T>, std::map<Key, Value>, std::optional<T> — todas están implementadas mediante plantillas.
Escribamos un ejemplo simple pero ilustrativo: una clase plantilla «Par» (análoga a std::pair):
template <typename First, typename Second>class Pair {private: First first_; Second second_;public: