Il meccanismo dell'ereditarieta` consente il riutilizzo di codice
precedentemente scritto, tuttavia il C++ offre un altro efficace meccanismo utile
allo scopo, i template (modelli).
Un template altro non e` che codice parametrico, dove i parametri possono essere
sia valori sia nomi di tipo. Tutto sommato questa non e` una grossa novita`, le
ordinarie funzioni sono gia` di per se del codice parametrico, solo che i parametri
possono essere unicamente valori di un certo tipo. C'e` comunque una differenza,
mentre le funzioni hanno pieno accesso ai loro parametri (possono leggerli e
modificarli), i template non consentono di realizzare codice che manipoli tipi
come se fossero valori, ma consentono di scrivere codice che operi su
tipi non noti fino a quando quel codice non sara` realmente utilizzato.
L'uso tipico dei template e` nella realizzazione di classi contenitore (liste,
stack, alberi...); anzicche` realizzare di volta in volta una lista
(o qualsiasi altro contenitore) per un certo tipo T il meccanismo dei
template consente di scrivere un tipo lista generica una volta per tutte, da
istanziare con una semplice dichiarazione nel programma che la deve utilizzare.
L'istanzazione consiste semplicemente nel indicare al compilatore il tipo T
degli oggetti che la lista dovra` contenere.
Questo comunque non e` l'unico utilizzo possibile dei template, in generale i
template consentono la stesura di codice generico di qualsiasi natura (anche
funzioni), alla base delle tecniche di programmazione generica sulla quale si basa
STL (Standard Template Library), una libreria di codice divenuta parte dell'ANSI C++.
Classi template
La definizione di codice generico e in particolare di
una classe template (le classi generiche vengono dette template class) non e`
molto complicata (anche se un po' laboriosa), la prima cosa che bisogna fare e`
dichiarare al compilatore la nostra intenzione di scrivere un template:
template < class T >Questa semplice dichiarazione (che non deve essere seguita da ";") dice al compilatore che la successiva dichiarazione (o definizione) utilizzera` un generico tipo T che sara` noto solo quando tale codice verra` effettivamente utilizzato.
template < class T > class TVector { public: TVector(int Size); ~TVector(); T & operator[](int Index); private: T * Data; int ArraySize; };L'esempio mostra come dichiarare una classe che incapsuli il concetto di array (ad esempio al fine di consentire la realizzazione di array a dimensione dinamica o che eseguano dei controlli sull'indice). Perche` la classe sia utilizzabile per ottenere array di qualsiasi tipo si e` fatto ricorso al meccanismo dei template in modo da dichiarare una classe che operi sul tipo generico T (unico parametro del template).
void main() { TVector< int > IntArray(30); TVector< TMyClass > MyArray(100); // array di elementi di tipo // definito dall'utente IntArray[5] = 27; IntArray[10] += IntArray[2]*5; MyArray[5].DoSomething(); MyArray[0].SomeFunc(IntArray[3]); /* ... */Le prime due righe della funzione mostrano come istanziare una classe template per dichiarare le variabili che la funzione utilizzera`: basta indicare il nome della classe template seguita dai nomi di tipo che devono essere sostituiti ai parametri del template (questi nomi vanno racchiusi tra parentesi angolari), i valori tra parentesi tonde sono i parametri del costruttore della classe TVector.
template < class T > TVector< T >::TVector(int Size) { ArraySize = Size; Data = Size? new T(Size) : 0; } template < class T > TVector< T >::~TVector() { if (Data) delete Data; } template < class T > T & TVector< T >::operator[] (int Index) { if (!ArraySize || (Index > ArraySize)) /* Segnalazione errore */ else return Data[Index]; }Un importante aspetto da tenere presente quando si scrivono template (siano essi calssi template o, come vedremo, funzioni) e` che la loro istanzazione possa richiedere che su uno o piu` dei parametri del template sia definita una qualche funzione. Esempio:
template < class T > class TOrderedList { public: /* ... */ T & First(); // Ritorna il primo valore // della lista void Add(T & Data); /* ... */ private: /* ... */ }; /* Definizione della funzione First() */ template < class T > void TOrderedList< T >::Add(T & Data) { /* ... */ T & Current = First(); if (T < Current) { // Attenzione qui! /* ... */ /* ... */ }la funzione Add tenta un confronto tra due valori di tipo T (parametro del template). La cosa e` perfettamente legale, solo che implicitamente si assume che sul tipo T sia definito operator < il tentativo di istanziare tale template con un tipo su cui tale operatore non e` definito e` pero` un errore che puo` essere segnalato solo quando il compilatore cerca di creare una istanza del template. E` quindi bene segnalare sempre con un commento assunzioni di questo genere.
template < class T > T & min(T & A, T & B) { return (A < B)? A : B; }Si noti che la definizione richiede implicitamente che sul tipo T sia definito operator <. In questo modo e` possibile calcolare il minimo tra due valori senza che sia definita una funzione min specializzata:
void main() { int A = 5; int B = 10; int C = min(A, B); TMyClass D(/* ... */); TMyClass E(/* ... */); TMyClass F = min(D, E); /* ... */ }Ogni qual volta il compilatore trova una chiamata alla funzione min istanzia (se non era gia stato fatto prima) la funzione template producendo una nuova funzione ed effettuando una chiamata a tale istanza. In sostanza con un template possiamo avere tutte le versioni overloaded della funzione min che ci servono.
template < class T > void F1(T); // Ok! template < class T > void F1(T *); // Ok! template < class T > void F1(T &); // Ok! template < class T > void F1(); // Errore! template < class T, class U > void F1(T, U); // Ok! template < class T, class U > void F1(T); // Errore!Questa restrizione non esiste per le classi template, perche` gli argomenti del template vengono specificati ad ogni istanza ogni qual volta si vuole creare un oggetto.
int A = min(10, 5); char B = min('a', 'A'); typedef unsigned int UINT; UINT C = 5; UINT D = 4; UINT E = min(C, D); /* ... */E` anche possibile avere valori come argomenti di un template, ad esempio una diversa definizione della classe TVector sarebbe stata:
template < class T, int Size > class TVector { public: T & operator[](int Index); private: T Data[Size]; }; template < class T, int Size > T & TVector< T, Size >::operator[] (int Index) { if (Index > Size) /* Segnalazione errore */ else return Data[Index]; }L'istanzazione in questo caso andrebbe fatta in questo modo:
TVector< char *, 20 > A; TVector< TMyClass, 30 > B;Si noti che il parametro Size e` utilizzato quasi come una costante e in effetti lo e` per via di come i template sono utilizzati dal compilatore. I parametri di un template sono utilizzati ovviamente anche per decidere quando due istanze di template sono equivalenti: due istanze di template sono equivalenti se i parametri utilizzati per la loro costruzione sono equivalenti nell'ordine in cui sono stati forniti:
TVector< char *, 20 > A; TVector< char *, 20 > B; TVector< char *, 40 > C; TVector< int, 40 > C; typedef char * TString; TVector< TString, 40 > C; int D = min(5, 7); int E = min(D, 3); char F = min('a', 'b');Le prime due istanze della template class TVector coincidono in quanto entrambi i parametri del template coincidono; la terza e` invece diversa dalle prime due in quanto il secondo parametro e` diverso, analogamente la terza con la quarta. La terza e la quinta istanza di TVector invece coincidono perche` la typedef crea un'alias e non un nuovo tipo.
TVector< TBaseShape *, 20 > A; TVector< TRectangle *, 20 > B; TVector< TTriangle *, 20 > C;tuttavia in questi casi gli oggetti A, B e C non sono legati da alcun vincolo di discendenza, indipendentemente dal fatto che le classi TBaseShape, TRectangle e TTriangle lo siano o meno, e` comunque possibile eseguire i seguenti assegnamenti:
A[5] = C[10]; A[8] = B[0];in quanto i singoli elementi dei vettori sono legati da un preciso vincolo di discendenza.
template< class T > class Base { /* ... */ }; template< class T > class Derived : public Base< T > { /* ... */ };in questo caso le istanze di classe template che si ottengono sono effettivamente legate da una relazione di discendenza, perche` e` esplicito nella definizione di Derived.