CodeGym /Cursos /C++ SELF /Plantillas y cabeceras

Plantillas y cabeceras

C++ SELF
Nivel 27, Lección 4
Disponible

1. Cómo funciona la plantilla: plano, no función

Hasta este punto del curso vivíamos según una regla bastante cómoda: «declaración — en la cabecera, implementación — en .cpp». Y esa regla es realmente buena: menos recompilaciones, menos ruido en la interfaz, menos posibilidades de crear un “cocedor” por medio de #include <everything>.

Pero las plantillas son como ese compañero que llega a la oficina y dice: «Vamos a hacerlo todo diferente, pero es por vuestro bien». Parece gracioso, pero es un hecho: en C++ hay dificultades reales ligadas a la visibilidad de las function templates y a efectos que surgen durante la búsqueda de nombres.

Para no hundirnos en la teoría (eso vendrá más adelante), tomamos exactamente una regla práctica y aprendemos a vivir según ella.

Una plantilla (template) no es «otra función más». Es más bien el plano de una función, según el cual el compilador puede crear una versión concreta para el tipo necesario. Es decir, la plantilla es una instrucción para el compilador: «Cuando encuentres un uso, sustituye el tipo y construye el cuerpo».

Es importante captar esta idea: el código de la plantilla el compilador lo "ensambla" allí donde se usa la plantilla. Por eso la plantilla tolera muy mal la situación en que la implementación está "en otro sitio" y no es visible en el punto de uso.

El ejemplo más pequeño de plantilla (y sí, es ese famoso “cuadrado” que ha sobrevivido a varias generaciones de estudiantes):

// square.hpp
#pragma once

template <typename T>
T square(T x) {
    return x * x;
}

Y su uso:

#include <iostream>
#include "square.hpp"

int main() {
    std::cout << square(5) << '\n';    // 25
    std::cout << square(2.5) << '\n';  // 6.25
}

Parece una función, pero en realidad es una “fábrica de funciones”.

2. Regla: el cuerpo de la plantilla debe ser visible donde se usa

Ahora viene una formulación que merece apuntarse en un papel y dejarlo junto al teclado (al lado de la pegatina “no olvides ;”):

Regla: la definición (el cuerpo) de la plantilla debe ser visible en el lugar donde se usa esa plantilla.

¿Por qué? Porque en el momento de uso el compilador puede comprender por primera vez qué tipos hay que instanciar. Y puesto que la sustitución ocurre ahí mismo, el cuerpo debe estar ahí mismo.

Imagen mental útil:

flowchart TD
    A["main.cpp vio square(5)"] --> B[hay que generar square
 
  ]
    B --> C{¿es visible el cuerpo de la plantilla?}
    C -- si --> D[el compilador genera el código]
    C -- no --> E[no hay base para generar: error]

 

Esto es toda la lección en un diagrama de flujo. Después solo lo “aterrizamos” en archivos.

4. Dónde guardar la implementación: .hpp y .tpp/.inl

Antiejemplo: declaraste en .hpp, definiste en .cpp

Ahora haremos un antiejemplo no porque nos guste sufrir, sino porque seguro que lo harás una vez en la vida. Y mejor verlo una vez en calma que en la noche antes de la entrega.

Imaginemos que tenemos una app de consola didáctica llamada TaskTracker (llevamos una lista de tareas). Queremos una utilidad pequeña: comprobar que un valor cae dentro de un rango (por ejemplo, una opción de menú de 1 a 5).

Creamos una cabecera con la declaración:

// range.hpp
#pragma once

template <typename T>
bool in_range(T x, T lo, T hi); // solo declaración

Y la implementación —en .cpp:

// range.cpp
#include "range.hpp"

template <typename T>
bool in_range(T x, T lo, T hi) {
    return lo <= x && x <= hi;
}

Y lo usamos en main.cpp:

#include <iostream>
#include "range.hpp"

int main() {
    std::cout << in_range(3, 1, 5) << '\n'; // queremos 1 (true)
}

A nivel humano todo parece perfecto: “pero si está la .cpp, ahí está la implementación”. Sin embargo el compilador piensa diferente: compila main.cpp por separado y en ese momento debe instanciar in_range<int>. Y el cuerpo no está —está en otra .cpp que se compila aparte, y no hay conexión telepática entre ellos.

Resultado: error de compilación (puede variar según compilador/IDE), pero la idea es una sola: la plantilla no puede instanciarse porque no hay definición donde se intenta usar.

Versión correcta: plantilla completa en la cabecera

Ahora hacemos lo que exige la regla: ponemos la definición de la plantilla donde será visible al #include. Es decir, en la .hpp.

Aquí va una versión que funciona:

// range.hpp
#pragma once

namespace app::util {

template <typename T>
bool in_range(T x, T lo, T hi) {
    return lo <= x && x <= hi;
}

} // namespace app::util

Y su uso en nuestro main.cpp:

#include <iostream>
#include "range.hpp"

int main() {
    const int choice = 3;
    std::cout << app::util::in_range(choice, 1, 5) << '\n'; // 1
}

Aquí el compilador está contento: cuando compila main.cpp, ve tanto la declaración como el cuerpo de in_range, por lo que puede “construir” la versión in_range<int>.

Autosuficiencia de la cabecera

Con plantillas, la autosuficiencia de la cabecera se vuelve más estricta. Si el cuerpo de la plantilla usa algún tipo/función, el #include necesario debe estar en esa misma cabecera, no “incluido antes en otro sitio”.

Por ejemplo, si in_range trabajara con cadenas y usáramos std::string, entonces <string> debería estar incluido en range.hpp.

Cómo no convertir .hpp en un papelote: .tpp / .inl

Cuando el proyecto crece, la cabecera con cuerpos de plantillas puede volverse demasiado larga. Es natural querer “mover la implementación” —y eso está bien. Simplemente no hay que moverla a .cpp, sino a un archivo que de todos modos se incluya desde la cabecera.

Patrón común (organizativo):

  • range.hpp — interfaz + inclusión de la implementación
  • range.tpp (o range.inl) — cuerpos de las plantillas

Ejemplo de range.hpp:

// range.hpp
#pragma once

namespace app::util {
template <typename T>
bool in_range(T x, T lo, T hi);
}

#include "range.tpp" // importante: incluimos la implementación

Ejemplo de range.tpp:

// range.tpp
#pragma once

namespace app::util {

template <typename T>
bool in_range(T x, T lo, T hi) {
    return lo <= x && x <= hi;
}

} // namespace app::util

La idea es simple: físicamente la implementación está en un archivo aparte, pero lógicamente sigue siendo visible mediante #include. La regla del día queda respetada.

5. Práctica: comprobación del menú en TaskTracker

Ahora damos un pequeño paso hacia la práctica (sin convertir la lección en “tarea”). Supongamos que en nuestra aplicación hay un menú: 1 — añadir tarea, 2 — mostrar lista, 3 — eliminar, 4 — salir. Queremos validar la entrada y no permitir, por ejemplo, la opción 42.

Mini fragmento de código (la idea es clara incluso sin el programa completo):

#include <iostream>
#include "range.hpp"

int main() {
    int choice = 0;
    std::cin >> choice;

    if (!app::util::in_range(choice, 1, 4)) {
        std::cout << "Opción de menú no válida\n";
    }
}

Fíjate, no hacemos nada “plantillado” en main.cpp. La plantilla está dentro de in_range. Es un buen estilo: main debe ser simple, y las utilidades — reutilizables.

Si hace falta otra analogía, la plantilla es como una receta donde el ingrediente “carne” no está especificado. Hasta que no llegues a la cocina y digas “hoy con pollo”, la receta sigue siendo “general”.

El lugar de uso es el momento en que dices: “Vale, hoy pollo”. Y aquí el cocinero (el compilador) debe tener la receta completa. Si solo tiene el encabezado “Haz algo sabroso”, y la receta está en otra ciudad (en .cpp), la cena será… digamos, conceptual.

6. Errores típicos

Error nº1: intentar esconder el cuerpo de la plantilla en .cpp “como una función normal”.
Esta es la trampa más común: el hábito del mundo de las funciones no plantilladas. Para una función normal basta la declaración y la implementación puede ir en .cpp. Para una plantilla no: el compilador debe ver el cuerpo donde genera la versión concreta. Si olvidas esto, el proyecto empieza a “romperse no de inmediato”, sino cuando la plantilla se usa por primera vez de verdad.

Error nº2: la cabecera de plantilla no es autosuficiente y “funciona por casualidad”.
Con las plantillas las dependencias transitivas son especialmente traicioneras. Hoy en algún sitio incluíste <string> antes —y todo funciona. Mañana cambias el orden de los #include (o otro archivo incluye tu cabecera primero) —y de repente “std::string no se encuentra”. Se soluciona con disciplina: si un tipo/función es necesario en la definición, inclúyelo en esa misma .hpp.

Error nº3: using namespace ...; en una cabecera con plantillas.
Una cabecera se incluye en muchos archivos, y si de repente “inyecta” nombres en el espacio global, las consecuencias serán extrañas y a veces muy divertidas (pero no te reirás tú; se reirá el compilador). En cabeceras de plantillas esto es aún más peligroso porque suelen incluirse “en todas partes”.

Error nº4: plantilla demasiado “inteligente” como parte de la capa utilitaria básica.
Aparece la tentación de crear la “súper cosa universal”: plantilla sobre plantilla, más decltype(auto), más trucos. En esta fase del curso eso casi siempre reduce la legibilidad más de lo que aporta beneficios. El sentido actual de las plantillas no es la potencia, sino el entendimiento de la regla de ubicación del código. Todo lo demás lo dejamos conscientemente para más adelante.

Error nº5: olvidar que la plantilla se compila “en el lugar de uso” y obtener errores donde no los esperas.
Esto es psicológicamente desagradable: arreglas la utilidad en range.hpp, y los errores aparecen en cinco diferentes .cpp. Es normal: esos .cpp instancian tu plantilla. Por eso las cabeceras con plantillas requieren especial cuidado y dependencias mínimas.

1
Tarea
C++ SELF, nivel 27, lección 4
Bloqueada
Botón cuadrado
Botón cuadrado
1
Tarea
C++ SELF, nivel 27, lección 4
Bloqueada
Lote de etiquetas
Lote de etiquetas
1
Tarea
C++ SELF, nivel 27, lección 4
Bloqueada
Resumen de estadísticas
Resumen de estadísticas
1
Tarea
C++ SELF, nivel 27, lección 4
Bloqueada
Comprobación de existencia
Comprobación de existencia
1
Cuestionario/control
Header hygiene, nivel 27, lección 4
No disponible
Header hygiene
Header hygiene
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION