Hermosos templates en C++ (parte II)

Como muchos saben, soy un amante de las automatizaciones de tareas, especialmente en mi trabajo: scripts, macros… templates! Hace tiempo que no hacía un segundo post al respecto, pero acá va.

Un asuntito que muchos saben pero a la vez no, es que un template debe estar completamente visible a la hora de especializarlo. En cristiano: el código de la clase (o función) debe estar en el “.h”. Esto es algo que sabemos todos; si no se hace eso, el compilador (mejor dicho, el “linker”) se queja con un “unresolved external symbol” o parecido. Esto es así y nada podemos hacer, el compilador necesita tener visible el código de la clase para poderla especializar. No hay nada que hacer, estamos 100% seguros de ello y tendremos que conformarnos con incluir cientos de líneas de código en cada fichero de cabecera, con la consiguiente ralentización de la compilación.

¿O no? ¿Hay alguna manera…? ¿Igual…?

Pues sí, hay una forma de evitar poner todo el código en el “.h” y tenerlo en nuestros queridos “.cpp”.

Ahora bien, hay que cumplir una premisa: conocemos los tipos de datos a los que se especializará la clase y el template no es más que una ayuda para evitar reescribir código, no una generalización completa. Además, el “ahorro” de tiempo de compilación será sólo visible si estamos creando una biblioteca (ya que la compilaremos una sola vez, mientras que habrán muchos proyectos que la usen). ¡Al código!

Imaginemos que el fichero “myclass.h” contiene a una clase sencilla:

template< typename T > class MyClass {
public:
	MyClass();
	~MyClass();

	void setA(T a) { _a = a; }
	T getA() { return _a; }

	void calc();

protected:
	T _a;
};

Ahora bien, “myclass.cpp”:

template< typename T > MyClass< T >::MyClass() {
	std::cout << \\"MyClass()\\" << std::endl;
	_a = static_cast< T >(0);
}

template< typename T > MyClass< T >::~MyClass() {
	std::cout << \\"~MyClass()\\" << std::endl;
}

template< typename T > void MyClass< T >::calc() {
	std::cout << \\"calc()\\" << std::endl;
	// a lot of useful code...
	_a = _a + _a;
}

Como vemos, la clase MyClass está pensada para operar con números, pero el método calc() es muy grande ;), y no conviene ponerlo en ".h", además, somos un poco cerrados y no queremos que nadie sepa cómo se inicializan los objetos de nuestra clase. Todo eso debe estar en un ".cpp".

Pero volvemos al problema de especialización. Alguien incluye el fichero "myclass.h", enlaza con nuestra súper biblioteca, especializa e instancia la clase con parámetro entero, y usa el método calc(). Cuando compile obtendrá un hermoso "unresolved external symbol" y se quedará con cara de circunstancias pensando en nuestra querida madre y en el paquete de cereales del que sacamos nuestro título.

Ahora, el meollo del post: ¿cómo resolverlo?. La solución pasa por la forma en la que los compiladores funcionan. Éstos generan tablas con todas las declaraciones de métodos, así como con sus definiciones (código) cuando las encuentran. Esto es, rudimentariamente hablando, una biblioteca: una gran tabla de definiciones.

Vayamos al programa que usa la clase. Cuando llamamos al método calc(), el compilador primero busca si está declarado (y lo está, dado que tenemos el ".h"), y cuando vaya a generar el ejecutable, buscará el código correspondiente, el cual no encuentra porque la especialización sólo ha podido tener lugar con las definiciones que había en el ".h", y la biblioteca (que sí conoce la definición) no lo tiene porque no se especializó.

Dado que sabemos los tipos de datos con los que trabajaremos (recordemos que era la premisa de la solución), podemos forzar al compilador a generar el código que necesitamos dentro de la biblioteca, especializando dichas clases en el mismo fichero ".cpp" de la clase.

template< typename T > void __MyClass_specialization__() {
	MyClass< T > foo;
	foo.calc();
}

void __all_MyClass_specializations__() {
	__MyClass_specialization__< int >();
	__MyClass_specialization__< short >();
	__MyClass_specialization__< long >();
	__MyClass_specialization__< float >();
	__MyClass_specialization__< double >();
}

¡Y listo! ahora cualquiera de las especializaciones estará disponible, la compilación de las aplicaciones será rápida (recordemos que calc() es un método súper complicado y largo) y no habremos tenido que reescribir varias veces el mismo código.

Un par de comentarios finales de esta solución. Es interesante ver que no hace falta llamar a la función __all_MyClass_specializations__(); el simple hecho de que esté presente ya hace que el compilador genere el código necesario, por lo que los métodos que se llamen dentro de la función mágica __MyClass_specialization__() no tienen que tener coherencia entre sí, ni parámetros válidos ni estar completamente inicializados. Lo importante es llamar a todos los métodos que no estén definidos en el ".h" sino en el ".cpp", salvando aquéllos que sean llamados por otros (por ejemplo, si el método _long_calc() es llamado desde calc(), el primero no hay que ponerlo en la función mágica). El constructor se crea al declarar el objeto "foo" (si hay varios constructores habrá que crear varios objetos, uno por constructor), y el destructor se especializa también dado que el objeto no es un dinámico.

Finalmente, dado que una clase puede tener muchos métodos, la función mágica es un template también para simplificar la actualización de la clase y la inclusión de nuevos tipos de datos.