Template


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.
Si puo` quindi procedere come al solito (per completezza l'esempio riparte da capo):
    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).
Supponendo di aver completamente definito la precedente classe, quello che bisogna fare per utilizzarla e` istanziare la dichiarazione di TVector:
    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.
Quando il compilatore incontra una dichiarazione di questo tipo per prima cosa istanzia il template, in pratica prende la dichiarazione (e la definizione) della classe template e costruisce automaticamente una nuova classe sostituendo ai parametri template (solo T nel nostro caso) i nomi di tipo specificati tra le parentesi angolari nell'ordine in cui si presentano. Alla fine il compilatore sa come e` fatta la classe TVector<int> ed e` in grado di creare la variabile IntArray.
Resta ancora da chiarire come definire la classe template TVector. Semplicemente bisogna definire uno per uno le funzioni membro della classe, ricordando che la keyword template si applica ad una sola dichiarazione (o definizione) per volta, e` anche necessario in un certo senso "istanziare" il template (non si tratta comunque di una vera istanzazione):
    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.



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 STL definisce la funzione min piu` o meno in questo modo:
    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.
Perche` sia possibile utilizzare i template con le funzioni e` necessari che ogni parametro del template sia utilizzato come tipo di almeno uno dei parametri della funzione, altrimenti istanziando il template non e` possibile ottenere una nuova versione overloaded distinguibile dalle precedenti:

  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.



Argomenti dei template

Un template puo` avere un qualsiasi numero di parametri e sebbene nella definizione il generico parametro sia indicato con la notazione class T (es. template <class T>) non e` necessario che all'atto dell'istanzazione esso sia sostituito da un identificatore di classe, puo` essere anche utilizzato un tipo primitivo o uno ottenuto tramite struct, enum, union o typedef. Esempio, riferendoci alla precedente definizione di min):
    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.
Le prime due istanze di min coincidono, perche` coincidono i parametri del template; la terza invece e` diversa.



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 TVector:
    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.
La cosa puo` sembrare strana, ma tutto sommato e` una conseguenza del modo in cui sono generate le istanze dei template che non consente la conservazione della relazione di discendenza. Diverso e` il caso in cui si deriva da una classe template:
    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.



Conclusioni

I template sono sicuramente un valido aiuto nel riutilizzo del codice e consentono la risoluzione di problemi altrimenti non risolvibili. Questo e` alla base di una recente diatriba tra promotori e detrattori di questo strumento. Da una parte personaggi come Alexander Stephanov (uno degli artefici di STL, il piu` famoso) sostengono strenuamente questa caratteristica del linguaggio, sottolineando come la programmazione generica (quella per template) risolva le limitazioni di cui soffre la OOP; tuttavia e` stato fatto notare che i problemi cui Stephanov accenna non sono intrinsechi della OOP, ma solo della sua implementazione in C++. Sebbene non evidente esistono alcune situazioni che in C++ con la sola OOP non e` possibile risolvere efficacemente, sostanzialmente per due motivi:
  1. Il C++ non e` un linguaggio a oggetti puro, i tipi fondamentali non sono classi, ne si comportano come tali;
  2. L'implementazione del polimorfismo non segue la regola della covarianza, in sostanza non e` possibile restringere il tipo degli argomenti di una funzione membro ridefinita in una classe derivata.
E` altresi` stato fatto notare come il meccanismo dei template altro non sia che una particolare implementazione del polimorfismo (polimorfismo parametrico).
Al di la` di tali discussioni, e` comunque indubbio che in certe situazioni i template sono necessari nel C++, sebbene molte cose (ad esempio le classi contenitrici) siano ottenibili con la OOP del linguaggio (sebbene con dei limiti derivanti da 1.).
Un'altra considerazione e` che in generale l'uso dei template porta ad ottenere eseguibili di grandi dimensioni, questo poiche` l'istanzazione di un template duplica porzioni di codice che con la OOP non produrrebbe; si pensi al caso di una lista, con la OOP in un linguaggio puro in cui ogni tipo discende da un tipo base, si sarebbe potuto realizzare una lista i cui elementi sono di tipo Base, poiche` in tali linguaggi ogni tipo e` sottotipo di Base la lista potrebbe contenere qualsiasi tipo di oggetto e non sarebbero state necessarie piu` istanze del tipo lista.


Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - © Copyright 1997, Paolo Marotta