Template
Il meccanismo dell'ereditarieta` consente il riutilizzo di
codice precedentemente scritto, l'idea e` quella di riconoscere le
proprieta` di un certo insieme di valori (tipo) e di definirle realizzando
una classe base (astratta) da specializzare poi caso per caso secondo le
necessita`. Quando riconosciamo che gli oggetti con cui si ha a che fare
sono un caso particolare di una qualche classe della gerarchia, non
si fa altro che specializzarne la classe piu` opportuna.
Esiste un'altro approccio che per certi versi procede in senso opposto;
anzicche` partire dai valori per determinarne le proprieta`, si definiscono
a priori le proprieta` scrivendo codice che lavora su tipologie (non note) di oggetti che soddisfano tali proprieta` (ad esempio l'esistenza di una
relazione di ordinamento) e si riutilizza tale codice ogni qual volta si
scopre che gli oggetti con cui si ha a che fare soddisfano quelle
proprieta`.
Quest'ultima tecnica prende il nome di programmazione generica ed
il C++ la rende disponibile tramite il meccanismo dei template.
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.
Classi contenitore
Supponiamo di voler realizzare una lista
generica facilmente riutilizzabile. Sulla base di quanto visto fino
ad ora l'unica soluzione possibile sarebbe quella di realizzare una
lista che contenga puntatori ad una generica classe TInfo
che rappresenta l'interfaccia di un generico oggetto memorizzabile nella
lista:
|
class TInfo {
/* ... */
};
class TList {
public:
TList();
~TList();
void Store(TInfo* Object);
/* ... */
private:
class TCell {
public:
TCell(TInfo* Object, TCell* Next);
~TCell();
TInfo* GetObject();
TCell* GetNextCell();
private:
TInfo* StoredObject;
TCell* NextCell;
};
TCell* FirstCell;
};
TList::TCell::TCell(TInfo* Object, TCell* Next)
: StoredObject(Object), NextCell(Next) {}
TList::TCell::~TCell() {
delete StoredObject;
}
TInfo* TList::TCell::GetObject() {
return StoredObject;
}
TList::TCell* TList::TCell::GetNextCell() {
return NextCell;
}
TList::TList() : FirstCell(0) {}
TList::~TList() {
TCell* Iterator = FirstCell;
while (Iterator) {
TCell* Tmp = Iterator;
Iterator = Iterator -> GetNextCell();
delete Tmp;
}
}
void TList::Store(TInfo* Object) {
FirstCell = new TCell(Object, FirstCell);
}
|
L'esempio mostra una parziale implementazione di una tale lista
(che assume la proprieta` degli oggetti contenuti), nella
realta` TInfo e/o TList molto probabilmente
sarebbero diverse al fine di fornire un meccanismo per eseguire delle
ricerche all'interno della lista e varie altre funzionalita`. Tuttavia il
codice riportato e` sufficiente ai nostri scopi.
Una implementazione di questo tipo funziona, ma soffre di (almeno) un grave
difetto: la lista puo` memorizzare tutta una gerarchia di oggetti, e
questo e` utile e comodo in molti casi, tuttavia in molte situazioni
siamo interessate a liste di oggetti omogenei e una soluzione di questo
tipo non permette di verificare a compile time che gli oggetti memorizzati
corrispondano tutti ad uno specifico tipo. Potremmo cercare (e trovare)
delle soluzioni che ci permettano una verifica a run time, ma non a compile
time. Supponete di aver bisogno di una lista per memorizzare figure
geometriche e una'altra per memorizzare stringhe, nulla vi impedisce di
memorizzare una stringa nella lista delle figure geometriche (poiche` le
liste memorizzano puntatori alla classe base comune TInfo).
Sostanzialmente una lista di questo tipo annulla i vantaggi di un type
checking statico.
Alcune osservazioni...
In effetti non e` necessario che il compilatore fornisca il codice macchina
relativo alla lista ancora prima che un oggetto lista sia istanziato,
e` sufficiente che tale codice sia generabile nel momento
in cui e` noto l'effettivo tipo degli oggetti da memorizzare.
Supponiamo di avere una libreria di contenitori generici (liste, stack...),
a noi non interessa il modo in cui tale codice sia disponibile, ci
basta poter dire al compilatore "istanzia una lista di stringhe", il
compilatore dovrebbe semplicemente prendere la definizione di lista
data sopra e sostituire al tipo TInfo il tipo
TString e quindi generare il codice macchina relativo
ai metodi di TList. Naturalmente perche` cio` sia possibile
il tipo TString dovrebbe essere conforme alle specifiche
date da TInfo, ma questo il compilatore potrebbe agevolmente
verificarlo. Alla fine tutte le liste necessarie sarebbero disponibili e
il compilatore sarebbe in grado di eseguire staticamente tutti i controlli
di tipo.
Tutto questo in C++ e` possibile tramite il meccanismo dei template.
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, la prima cosa che bisogna
fare e` dichiarare al compilatore la nostra intenzione di scrivere un
template utilizzando appunto la keyword template:
template < class T >
Questa semplice dichiarazione (che non deve essere seguita da ";")
dice al compilatore che la successiva dichiarazione utilizzera`
un generico tipo T che sara` noto solo quando tale codice verra`
effettivamente utilizzato, il compilatore deve quindi memorizzare
quanto segue un po' cose se fosse il codice di una funzione
inline per poi istanziarlo nel momento in cui T
sara` noto.
Vediamo come avremmo fatto per il caso della lista vista sopra:
|
template < class TInfo >
class TList {
public:
TList();
~TList();
void Store(TInfo& Object);
/* ... */
private:
class TCell {
public:
TCell(TInfo& Object, TCell* Next);
~TCell();
TInfo& GetObject();
TCell* GetNextCell();
private:
TInfo& StoredObject;
TCell* NextCell;
};
TCell* FirstCell;
};
|
Al momento l'esempio e` limitato alle sole dichiarazioni, vedremo
in seguito come definire i metodi del template.
Intanto, si noti che e` sparita la dichiarazione della classe
TInfo, la keyword template dice al compilatore che
TInfo rappresenta un nome di tipo qualsiasi (anche un tipo
primitivo come int o long double). Le dichiarazioni quindi
non fanno piu` riferimento ad un tipo esistente, la` dove e` stato
utilizzato il nome fittizio TInfo.
Inoltre il contenitore non memorizza piu` tipi puntatore, ma riferimenti
alle istanze di tipo.
Supponendo di aver fornito anche le definizioni dei metodi, vediamo come
istanziare la generica lista:
|
TList < double > ListOfReal;
double* AnInt = new double(5.2);
ListOfReal.Store(*AnInt);
TList < Student > MyClass;
Student* Pippo = new Student(/* ... */);
ListOfReal.Store(*Pippo); // Errore!
MyClass.Store(*Pippo); // Ok!
|
La prima riga istanzia la classe template TList sul tipo
double in modo da ottenere una lista di double; si noti
il modo in cui e` stata istanziato il template ovvero tramite
la notazione
NomeTemplate < Tipo >
(si noti che Tipo va specificato tra parentesi angolate).
Il tipo di ListOfReal e` dunque TList < double
>.
Successivamente viene mostrato l'inserzione di un double e il
tentativo di inserimento di un valore di tipo non opportuno, l'errore
sara` ovviamente segnalato in fase di compilazione.
La definizione dei metodi di TList avviene nel seguente
modo:
|
template < class TInfo >
TList < TInfo >::
TCell::TCell(TInfo& Object, TCell* Next)
: StoredObject(Object), NextCell(Next) {}
template < class TInfo >
TList < TInfo >::TCell::~TCell() {
delete &StoredObject;
}
template < class TInfo >
TInfo& TList < TInfo >::TCell::GetObject() {
return StoredObject;
}
template < class TInfo >
TList < TInfo >::TCell*
TList < TInfo >::TCell::GetNextCell() {
return NextCell;
}
template < class TInfo >
TList < TInfo >::TList() : FirstCell(0) {}
template < class TInfo >
TList < TInfo >::~TList() {
TCell* Iterator = FirstCell;
while (Iterator) {
TCell* Tmp = Iterator;
Iterator = Iterator -> GetNextCell();
delete Tmp;
}
}
template < class TInfo >
void TList < TInfo >::Store(TInfo& Object) {
FirstCell = new TCell(Object, FirstCell);
}
|
Cioe` bisogna indicare per ogni membro che si tratta di codice relativo
ad un template e contemporaneamente occorre istanziare la
classe template utilizzando il parametro del template.
Un template puo` avere un qualsiasi numero di parametri non
c'e` un limite prestabilito; supponete ad esempio di voler realizzare
un array associativo, l'approccio da seguire richiederebbe un template con due parametri e una soluzione potrebbe essere la
seguente:
|
template < class Key, class Value >
class AssocArray {
public:
/* ... */
private:
static const int Size;
Key KeyArray[Size];
Value ValueArray[Size];
};
template < class Key, class Value >
const int AssociativeArray < Key, Value >::Size = 100;
|
Questa soluzione non pretende di essere ottimale, in particolare soffre
di un limite: la dimensione dell'array e` prefissata. Fortunatamente
un template puo` ricevere come parametri anche valori di un certo
tipo:
|
template < class Key, class Value, int size >
class AssocArray {
public:
/* ... */
private:
static const int Size;
Key KeyArray[Size];
Value ValueArray[Size];
};
template < class Key, class Value, int size >
const int AssocArray < Key, Value, size >::Size = size;
|
La keyword typename
Consideriamo il seguente
esempio:
|
template < class T >
class TMyTemplate {
public:
/* ... */
private:
T::TId Object;
};
|
E` chiaro dall'esempio che l'intenzione era quella di utilizzare un
tipo dichiarato all'interno di T per istanziarlo all'interno del
template.
Tale codice puo` sembrare corretto, ma in effetti il compilatore non produrra` il risultato voluto. Il problema e` che il compilatore non puo` sapere in anticipo se T::TId e` un identificatore di tipo
o un qualche membro pubblico di T.
Per default il compilatore assume che TId sia un membro
pubblico del tipo T e` l'unico modo per ovviare a cio` e`
utilizzare la keyword typename introdotta dallo standard:
|
template < class T >
class TMyTemplate {
public:
/* ... */
private:
typename T::TId Object;
};
|
La keyword typename indica al compilatore che l'identificatore che
la segue deve essere trattato come un nome di tipo, e quindi nell'esempio
precedente Object e` una istanza di tale tipo. Si ponga
attenzione al fatto che typename non sortisce l'effetto di una
typedef, se si desidera dichiarare un alias per T::TId
il procedimento da seguire e` il seguente:
|
template < class T >
class TMyTemplate {
public:
/* ... */
private:
typedef typename T::TId Alias;
Alias Object
};
|
Un altro modo corretto di utilizzare typename e` nella dichiarazione
di template:
|
template < typename T >
class TMyTemplate {
/* ... */
};
|
In effetti se teniamo conto che il significato di class in una
dichiarazione di template e` unicamente quella di indicare un nome di tipo
che e` parametro del template e che tale parametro puo` non essere una
classe (ma anche int o una struct, o un
qualsiasi altro tipo), si capisce come sia piu` corretto utilizzare
typename in luogo di class.
La ragione per cui spesso troverete class invece di
typename e` che prima dello standard tale keyword non
esisteva.
Vincoli impliciti
Un importante aspetto da tenere presente
quando si scrivono e si utilizzano template (siano essi classi
template o, come vedremo, funzioni) e` che la loro istanziazione
possa richiedere che su uno o piu` dei parametri del template
sia definita una qualche funzione o operazione. Esempio:
|
template < typename T >
class TOrderedList {
public:
/* ... */
T& First(); // Ritorna il primo valore
// della lista
void Add(T& Data);
/* ... */
private:
/* ... */
};
/* Definizione della funzione First() */
template < typename T >
void TOrderedList< T >::Add(T& Data) {
/* ... */
T& Current = First();
while (Data < 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.
Purtroppo il linguaggio segue la via dei vincoli impliciti, ovvero non
fornisce alcun meccanismo per esplicitare assunzioni fatte sui
parametri dei template, tale compito e` lasciato ai messaggi di
errore del compilatore e alla buona volonta` dei programmatori che
dovrebbero opportunamente commentare situazioni di questo genere.
Problemi di questo tipo non ci sarebbero se si ricorresse al polimorfismo,
ma il prezzo sarebbe probabilmente maggiore dei vantaggi.
Funzioni template
Oltre a classi template e` possibile
avere anche funzioni template, utili quando si vuole definire solo
un'operazione e non un tipo di dato, ad esempio la libreria standard
definisce la funzione min piu` o meno in questo modo:
|
template < typename 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:
|
int main(int, char* []) {
int A = 5;
int B = 10;
int C = min(A, B);
TMyClass D(/* ... */);
TMyClass E(/* ... */);
TMyClass F = min(D, E);
/* ... */
return 0;
}
|
Ogni qual volta il compilatore trova una chiamata alla funzione
min istanzia (se non era gia stato fatto prima) la
funzione template (nel caso delle funzioni l'istanziazione e` un
processo totalmente automatico che avviene quando il compilatore
incontra una chiamata alla 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 con un'unica definizione.
Si osservi che affinche la funzione possa essere correttamente istanziata,
i parametri del template devono essere utilizzati nella lista
dei parametri formali della funzione in quanto il compilatore istanzia
le funzioni template sulla base dei parametri attuali specificati
al momento della chiamata:
|
template < typename T > void F1(T);
template < typename T > void F1(T*);
template < typename T > void F1(T&);
template < typename T > void F1(); // Errore
template < typename T, typename U > void F1(T, U);
template < typename T, typename U > int F1(T);// Errore
|
Questa restrizione non esiste per le classi template, perche` gli
argomenti del template vengono specificati esplicitamente ad ogni
istanziazione.
Template ed ereditarieta`
E` possibile utilizzare contemporaneamente
ereditarieta` e template in vari modi. Supponendo di avere una gerarchia di
figure geometriche potremmo ad esempio avere le seguenti istanze di
TList:
|
TList < TBaseShape > ShapesList;
TList < TRectangle > RectanglesList;
TList < TTriangle > TrianglesList;
|
tuttavia in questi casi gli oggetti ShapesList,
RectanglesList e TrianglesList non sono legati
da alcun vincolo di discendenza, indipendentemente dal fatto che le classi
TBaseShape, TRectangle e
TTriangle lo siano o meno.
Naturalmente se TBaseShape e` una classe base delle
altre due, e` possibile memorizzare in ShapesList anche
oggetti TRectangle e TTriangle perche` in
effetti TList memorizza dei riferimenti, sui quali valgono
le stesse regole per i puntatori a classi base.
Istanze diverse dello stesso template non sono mai legate dunque
da relazioni di discendenza, indipendentemente dal fatto che lo siano
i parametri delle istanze del template. La cosa non e` poi tanto
strana se si pensa al modo in cui sono gestiti e istanziati i
template.
Un altro modo di combinare ereditarieta` e template e` dato
dal seguente esempio:
|
template< typename T >
class Base {
/* ... */
};
template< typename T >
class Derived : public Base< T > {
/* ... */
};
Base < double > A;
Base < int > B;
Derived < int > C;
|
in questo caso l'ereditarieta` e` stata utilizzata per estendere le
caratteristiche della classe template Base,
Tuttavia anche in questo caso tra le istanze dei template
non vi e` alcuna relazione di discendenza, in particolare non
esiste tra B e C; un puntatore a
Base < T > non potra` mai puntare
a Derived < T >.
Conclusioni
Template ed ereditarieta` sono strumenti assai
diversi pur condividendo lo stesso fine: il riutilizzo di codice gia`
sviluppato e testato. In effetti la programmazione orientata agli
oggetti e la programmazione generica sono due diverse scuole di pensiero
relative al modo di implementare il polimorfismo (quello della OOP e` detto
polimorfismo per inclusione, quello della programmazione generica invece
e` detto polimorfismo parametrico). Le conseguenze dei due approcci sono
diverse e diversi sono i limiti e le possibilita` (ma si noti che tali
differenze dipendono anche da come il linguaggio implementa le due
tecniche). Tali differenze non sempre comunque pongono in antitesi i due
strumenti: abbiamo visto qualche limite del polimorfismo della OOP
nel C++ (ricordate il problema delle classi contenitore) e abbiamo visto il
modo elegante in cui i template lo risolvono. Anche i
template hanno alcuni difetti (ad esempio quello dei vincoli
impliciti, o il fatto che i template generano
eseguibili molto piu grandi) che non troviamo nel polimorfismo della OOP.
Tutto cio` in C++ ci permette da un lato di scegliero lo strumento che piu`
si preferisce (e in particolare di scegliere tra un programma basato su OOP
e uno su programmazione generica), e dall'altra parte di rimediare ai
difetti dell'uno ricorrendo all'altro. Ovviamente saper mediare tra i due
strumenti richiede molta pratica e una profonda conoscenza dei meccanismi
che stanno alla loro base.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|