In e C/C++ si ha la convenzione di suddividere il codice all’interno di due file: i file .h per le dichiarazioni e .cpp o .c. Le motivazioni di questa scelta, in realtà, vanno oltre il semplice vezzo stilistico. Tale suddivisione permette infatti una profonda modularità del software e una rudimentale forma di information-hiding. I vari moduli (cpp) quando richiamano un altro modulo (tramite include) vengono a conoscenza solamente delle dichiarazioni delle funzioni e mai della loro implementazione. Implementazione che, inoltre, non gli servirebbe a nulla.
Capita così che il novello programmatore C++ (nostro ormai noto compagno di avventure), conscio di questa convenzione, inizia a scrivere le sue classi con disinvoltura. Suddivide tutte le sue classi con cura ma pecca di solerzia: divide anche le classi template.
Le classi template sono delle classi che hanno uno (o più) tipi parametrizzati. Supponiamo infatti di voler scrivere una struttura dati (ad esempio un array)per organizzare Pere. Poi decidiamo di scrivere una struttura dati analoga solo per gestire Autoradio. Ci rendiamo conto che la struttura fondamentale delle due classi è la stessa. Senza template dovremmo scrivere due classi differenti mentre con le classi template ci basta farne una parametrizzata. Vediamo un esempio:
template
class MyArray
{
std::vector data;
public:
void Add(T *d);
void Remove();
void Print();
};
Come potete notare all’interno di questo scheletro di classe il tipo gestito non è esplicito ma indicato da un segnaposto T. Quando inizializziamo una nuova coda non ci resta che inizializzarla specificando il tipo parametrizzato, ad esempio in questo modo:
MyArray<Pere> lista_pere = new MyArray<Pere>
Ma come funziona questo artificio stregonesco? Molto semplice: in pratica è il compilatore che si incarica di scrivere le classi differenti basandosi sullo scheletro che voi avete dato.
Ed è proprio qui che cade la magagna! A causa di ciò, infatti, le classi template non possono essere divise in file .h e .cpp. se lo fate noterete che il vostro linker inizierà a lamentarsi vistosamente dicendovi che non esiste nessun MyArray<Pere>.
Il motivo di questo comportamento è logico se ci pensate. Supponiamo di avere diviso il nostro template in due file: MyArray.h e MyArray.cpp. Supponiamo poi di utilizzare il nostro template nel file Fruttivendolo.cpp. A questo punto compiliamo:
- Compiliamo tranquillamente il nostro template generando il file compilato MyArray.o.
- A questo punto andiamo a compilare Fruttivendolo.cpp.
- Come abbiamo detto prima il compilatore nota la riga
MyArray<Pere>
e quindi “costruisce” la classe corrispodente.
- Ma il file Fruttivendolo.cpp può vedere solo MyArray.h perché è il solo file incluso!
A questo punto il compilatore si porge giustamente la domanda: come faccio a compilare la classe MyArray<Pere> se non conosco come sono implementati i metodi del template MyArray.h?
Infatti non lo fa. Ignora il tutto lasciando il compito al linker e sperando che esso trovi da qualche parte un aggancio ad una classe MyArray<pere> che, come è giusto che sia, non trova.
Come risolvere il problema? La soluzione più semplice è di scrivere tutto (dichiarazione più implementazione) tutto nel file header. Tuttavia noi vogliamo mantenere una certa omogeneità nel codice e vogliamo quindi mantenere la divisione!
Per questo ci sono tre soluzioni accettabili. Ovviamente ognuna hai suoi vantaggi e svantaggi e sta a voi scegliere quale sia la migliore per la vostra esigenza:
- Nel file .h aggiungere alla fine la riga
#include MyArray.cpp
. In questo modo viene simulata la scrittura “tutto in uno” pur mantenendo la suddivisione del file. Ovviamente in questo caso nel file .cpp NON VA INCLUSO IL FILE .h per evitare un inclusione ciclica. Questa soluzione ha lo svantaggio di “gonfiare” il codice compilato nel caso lo stesso template venga utilizzato in parecchi file. - Una soluzione alternativa è di definire tutte le funzioni come inline. Anche qui abbiamo gli stessi svantaggi del caso precedente (forse sono anche peggio).
- La terza soluzione è l’ideale se sapete da subito quali sono i tipi di dato con cui utilizzerete il template. In questo caso vi basta aggiungere in fonfo al file .cpp la riga
template class MyArray<Pere>;
per forzare la compilazione del template per quei tipi particolari. A questo punto il linker troverà sicuramente una versione già compilate della classe che è stata appena chiamata.
Spero che questa piccola note, spesso trascurata quando si parla di template, vi faccia risparmiare tutto il tempo che ha fatto perdere a me a suo tempo.
Buon coding! 😀
In realtà ci sono compilatori (g++ sicuramente non è tra questi comunque) che non hanno questo problema.
Vero. Clang ad esempio gestisce la cosa in modo leggermente differente (anche se non posso confermarlo allo stato attuale).
Tuttavia la domanda è ricorrente e g++ continua a rimanere uno dei compilatori più utilizzati in assoluto. Quindi per il momento bisogna adattarsi 😉