Introducción: El dolor familiar de todo desarrollador de C++
\n\n¿Alguna vez ha escrito una clase plantilla ordenada, separó la declaración en un archivo .h y la implementación en un .cpp, compiló el proyecto y obtuvo del enlazador algo como "undefined reference to ..."? Si es así, no está solo. Esta pregunta en Stack Overflow ha acumulado miles de visitas y cientos de comentarios. El tema "why can templates only be implemented in the header file" es una verdadera piedra de tropiezo para los principiantes e incluso para programadores experimentados que migran a C++ desde otros lenguajes.
En este artículo, aclararemos de una vez por todas por qué las plantillas de C++ requieren este enfoque, cómo funciona el modelo de compilación y qué hacer si realmente desea ocultar la implementación en un .cpp. ¡Allá vamos!
1. ¿Cómo ve el compilador una plantilla? Modelo de compilación de C++
\n\nPara entender el problema, debemos recordar cómo C++ compila el código normal. El proceso consta de dos etapas:
\n\n- \n
- Compilación — cada archivo
.cpp(unidad de traducción) se compila de forma independiente. El compilador solo ve lo que se incluye en ese archivo a través de#include. \n - Enlace — los archivos objeto (
.oo.obj) se ensamblan, se buscan definiciones de funciones, variables y clases. \n
Ahora imagine que tiene una plantilla de función max:
// max.h\ntemplate <typename T>\nT max(T a, T b); // ¡Solo declaración!\n\n// max.cpp\n#include "max.h"\ntemplate <typename T>\nT max(T a, T b) {\n return (a > b) ? a : b;\n}\n\n// main.cpp\n#include "max.h"\nint main() {\n int result = max(3, 5); // ¡El compilador solo ve la declaración!\n return 0;\n}\n\nAl compilar main.cpp, el compilador encuentra la llamada max(3,5). Sabe que max es una plantilla, pero no ve su implementación. El compilador no puede instanciar la plantilla con el tipo int porque la definición está en otro archivo (max.cpp) que se compila por separado. Como resultado, genera una llamada con una referencia externa, y el enlazador no encuentra una función instanciada adecuada; de ahí el error.
Aquí radica la raíz del problema: las plantillas de C++ deben ser instanciadas por el compilador durante la compilación, y para ello, el compilador necesita ver la definición completa en cada unidad de traducción donde se use la plantilla.
\n\n2. ¿Por qué esto no funciona como con las funciones normales?
\n\nA diferencia de las funciones normales, una plantilla no es código listo. Es una "instrucción" para el compilador: "cuando veas un uso con el tipo int, genera una función para int". Si la implementación está oculta en un .cpp, el compilador en main.cpp no recibe esa instrucción y no puede crear el código necesario.
Imagine que tiene una receta (plantilla), pero la receta misma está en una caja fuerte (en otro archivo .cpp). El chef (compilador) solo ve el nombre del plato, pero no sabe cómo prepararlo. El plato no se logra. Para una función normal, la receta ya está lista: el chef simplemente toma el plato ya preparado del refrigerador (del archivo objeto).
3. Solución clásica: todo en el archivo de cabecera
\n\nLa forma más simple y común es colocar la implementación directamente en el archivo de cabecera. Esta es una técnica estándar, conocida como bibliotecas "header-only". Por ejemplo:
\n\n// max.h\n#ifndef MAX_H\n#define MAX_H\n\ntemplate <typename T>\nT max(T a, T b) {\n return (a > b) ? a : b;\n}\n\n#endif\n\nAhora, cualquier archivo que incluya max.h recibe la definición completa de la plantilla. El compilador puede instanciarla para cualquier tipo necesario. Así es como funcionan bibliotecas populares como Boost y la mayor parte de la STL. Desventaja