Menu

Plantillas en C++: funciones y clases genéricas explicadas

Escribe el código una sola vez y deja que funcione con cualquier tipo gracias a las plantillas de C++: plantillas de función, plantillas de clase, deducción de tipos y los confusos errores de compilación que provocan.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Escríbelo una vez, úsalo para cualquier tipo

En la página anterior ordenaste un vector<int> con std::sort. Pero std::sort también ordena un vector<string>, un vector<double> o un arreglo de tus propias estructuras, sin que nadie tenga que escribir un sort aparte para cada uno. Eso no es magia ni es sobrecarga. Es una plantilla: una única porción de código que el compilador reutiliza para cualquier tipo que le entregues.

Sin plantillas estarías condenado a copiar y pegar la misma lógica para cada tipo. Aquí está la misma función maximum escrita tres veces, justo la duplicación que las plantillas existen para eliminar:

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

Los cuerpos son idénticos. Solo cambian los tipos. Una plantilla te permite decir "esto funciona para cualquier tipo T" y escribirlo una sola vez.

Plantillas de función

Conviertes una función en una plantilla añadiendo template <typename T> delante de ella y usando T allí donde normalmente iría un tipo concreto.

Fíjate en que nunca escribiste maximum<int> ni maximum<double>. El compilador observa los argumentos y deduce qué debe ser T: eso es la deducción de argumentos de plantilla. Cada tipo distinto con el que la llamas hace que el compilador instancie (genere) una función concreta independiente entre bastidores.

Puedes indicar el tipo explícitamente cuando la deducción no sirve, usando corchetes angulares:

Un tropiezo común acecha en la deducción. Como T debe ser un único tipo, mezclar tipos de argumentos la rompe:

maximum(3, 7.5);   // ERROR: ¿T es int o double? El compilador se niega a adivinar.

Puedes corregirlo siendo explícito - maximum<double>(3, 7.5) - o dándole a cada parámetro su propio parámetro de tipo, que es lo que haremos a continuación.

Múltiples parámetros de tipo

Una plantilla no se limita a un solo tipo. Enumera tantos como necesites, separados por comas. Así es como escribes una función cuyos parámetros pueden ser de tipos diferentes:

Cuando el tipo de retorno depende de los parámetros, deja que el compilador lo resuelva con auto (C++14 en adelante), que combina de forma natural con las plantillas:

Plantillas de clase

Las plantillas no son solo para funciones: también se pueden parametrizar clases enteras. Así es exactamente como funcionan los contenedores estándar: vector<int>, el map clave-valor map<string, int> y pair<A, B> son todos plantillas de clase. Escribes la estructura de datos una sola vez y almacena el tipo que le indiques como parámetro.

Aquí tienes una pequeña Box genérica que guarda un valor de cualquier tipo:

La diferencia clave respecto a las plantillas de función: con una plantilla de clase normalmente tienes que indicar el tipo entre corchetes angulares - Box<int> - porque en los estándares más antiguos no hay argumentos de constructor de los que deducirlo. (C++17 añadió la deducción de argumentos de plantilla de clase, así que Box b(42); también funciona, pero ser explícito siempre es seguro y se lee con claridad).

Los errores serán enormes: aquí está el porqué

Esta es la parte con la que todo el mundo tropieza, así que vale la pena decirlo sin rodeos. Una plantilla solo se comprueba por completo cuando se instancia con un tipo real. Puedes escribir una plantilla que use < y que compile sin problemas por sí sola: el error solo aparece en el momento en que la instancias con un tipo que no tiene <.

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // requiere que T admita >
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// ERROR: no hay operator > para Point. El mensaje nombra Point Y
// cita esta función entera, a menudo abarcando muchas líneas.

Como el compilador sustituye el tipo completo dentro de la plantilla y reporta los fallos desde dentro del código generado, un solo error puede producir un muro de salida que menciona detalles internos de la biblioteca. Dos consejos de supervivencia:

  • Lee el primer error, no el último. Los errores posteriores suelen ser consecuencia del primero.
  • Busca en el mensaje el nombre de tu propio tipo (aquí, Point). Eso te dice qué instanciación salió mal.

La verdadera solución es asegurarte de que tu tipo admita lo que la plantilla necesita: para maximum, eso significa sobrecargar el operador operator> en Point, que es tema para una página posterior. Los concepts del moderno C++20 pueden adelantar estos errores y hacerlos legibles, pero el modelo de sustitución que hay debajo es el mismo.

Siguiente: Clases

Acabas de construir una plantilla de clase Box - una clase con datos privados, un constructor y funciones miembro - mientras te centrabas en la parte de las plantillas. La siguiente página baja el ritmo y enseña las clases como es debido: cómo agrupar los datos con las funciones que operan sobre ellos, qué controlan realmente public y private, y cómo las funciones miembro acceden al propio estado del objeto. Las plantillas y las clases se combinan constantemente en C++ real, así que dominar bien las clases hace que escribir código genérico sea mucho más fácil.

Preguntas frecuentes

¿Qué es una plantilla en C++?

Una plantilla es un molde que te permite escribir una función o clase una sola vez y dejar que el compilador genere una versión para cada tipo con el que la uses. Escribes template <typename T> y luego usas T como sustituto del tipo real. El compilador genera una versión concreta: a esto se le llama instanciación.

¿Cuál es la diferencia entre typename y class en una plantilla de C++?

En una lista de parámetros de plantilla, template <typename T> y template <class T> significan exactamente lo mismo. Hoy en día se prefiere typename porque resulta más honesto: T puede ser cualquier tipo, no solo una clase. La elección de la palabra clave no afecta en absoluto al código generado.

¿Por qué los mensajes de error de las plantillas en C++ son tan largos?

Las plantillas se comprueban cuando se instancian con un tipo real, no cuando se escriben. Si un tipo no admite una operación que usaste (como < para ordenar), el error aparece en lo más profundo del código de la biblioteca, con el tipo instanciado completo escrito por extenso, produciendo páginas enteras de salida. Lee el primer error y busca en él el nombre de tu tipo.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR