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