Esercitazioni di Informatica e Sistemi

Classi parametriche

I template (modelli di funzione) sono particolari funzioni che possono operare con tipi generici. In questo modo si crea lo schema di una funzione le cui operazioni possono essere adattate a più di un tipo, o classe, senza dover riscrivere l'intero corpo della funzione per ciascun tipo.

In C++ questo può essere ottenuto usando i parametri formali del template.

Un parametro formale del template è uno speciale parametro che può essere usato per passare un tipo come parametro ad una funzione, allo stesso modo in cui viene passato un parametro effettivo ad una funzione. I template di funzione possono usare il tipo ricevuto come parametro come se fosse un qualsiasi tipo predefinito.

Il formato della dichiarazione di un template di funzione che si aspetta di ricevere, al momento della chiamata, sia il tipo restituito sia il tipo dei parametri effettivi è:

    template <class identificatore> dichiarazione di funzione;

Ad esempio un template di funzione che restituisce il maggiore tra due elementi potrebbe essere dichiarato così:

template <class Tipo>
Tipo MassimoTra(Tipo a, Tipo b) {
  return (a>b ? a : b);
}

MassimoTra è un template di funzione con Tipo come parametro formale. Questo parametro rappresenta un tipo che non è stato ancora specificato ma che può essere usato nel template della funzione come se fosse un tipo predefinito. La funzione deve restituire il maggiore tra due parametri di questo tipo non ancora specificato.

La chiamata di questo template di funzione deve rispettare la seguente sintassi:

    Nome_funzione <Tipo> (parametri);

Ad esempio, per chiamare la funzione MaggioreTra per ottenere il più grande tra due caratteri x e y, basta sostituire Tipo con char:

    char x, y;
    MassimoTra <char> (x, y);

Quando il compilatore raggiunge questa chiamata al template della funzione, usa lo schema del template per generare automaticamente una funzione in cui sono stati sostituiti i termini Tipo con il tipo passato come parametro effettivo (char, in questo caso) e poi la chiama. Questa operazione è compiuta dal compilatore e non è visibile al programmatore. Il seguente esempio illustra un programma in cui è presente la dichiarazione e l'uso del template:

    #include <iostream>
    using namespace std;
    template <class Tipo>
    Tipo MassimoTra(Tipo a, Tipo b) {
      return (a>b ? a : b);
    }
    int main() {
      int i=5, j=6, k;
      long m=10, n=5, p;
      k = MassimoTra <int> (i, j);
      cout << k << " maggiore tra " << i << " e " << j << endl;
      p = MassimoTra <long> (m, n);
      cout << p << " maggiore tra " << m << " e " << n << endl;
      return 0;
    }

Nell'esempio precedente si è usato due volte il nome del template della funzione MassimoTra. La prima volta con il parametro di tipo int e la seconda volta con il parametro di tipo long. Il compilatore ha creato e poi chiamato l'appropriata versione della funzione.

All'interno del template della funzione MassimoTra, si potrebbe scrivere "Tipo risultato;" in cui Tipo è usato per dichiarare una nuova variabile di quel tipo. Quindi, risultato sarebbe una variabile dello stesso tipo dei parametri a e b quando la funzione associata al template verrà creata con uno specifico tipo.

In questo caso particolare il tipo generico Tipo è usato come parametro per MassimoTra e il compilatore può determinare automaticamente quale tipo di dato deve creare senza doverlo specificare esplicitamente tra i segni < e >, come è stato fatto specificando <int> e <long>. Infatti, si sarebbe potuto scrivere:

    int i, j;
    MassimoTra (i, j);

Poichè sia i che j sono di tipo int, il compilatore può automaticamente dedurre che il parametro del template può essere solo int. Questo metodo implicito produce esattamente lo stesso risultato.

Poichè il template della funzione include un solo parametro formale del template (class T) e il template della funzione stessa accetta due parametri, entrambi di questo stesso tipo T, non è possibile chiamare il template della funzione passando due parametri di tipo differente tra loro, come nel seguente caso:

    int i;
    long m;
    k = MassimoTra (i, m); // errore i e m sono di tipo diverso tra loro

Per poter passare parametri di tipi diversi, il template della funzione deve accettare più di un tipo come parametro, basta specificare nomi diversi per i parametri formale del template tra i segni < e >. Per esempio:

    template <class T, class U>
    T MinimoTra (T a, U b) {
      return (a<b ? a : b);
    }

In questo caso il template della funzione MinimoTra() accetta due parametri di tipi diversi e restituisce un valore dello stesso tipo del primo parametro (T) che viene passato. Ad esempio, dopo la dichiarazione si potrebbe chiamare la funzione MinimoTra() con la seguente espressione:

    int i, j;
    long m;
    i = MinimoTra<int, long> (j, m);
o semplicemente:
    i = MinimoTra (j, m);

persino se j ed m hanno tipo differente, perchè il compilatore lo può dedurre.

Classi template

Si possono scrivere classi parametriche (template di classi) allo scopo di dichiarare campi membro che usino i parametri formali del template come tipo. Ad esempio:

    template <class T>
    class coppia {
        T valori[2];
      public:
        coppia (T primo, T secondo) {
          valori[0]=primo;
          valori[1]=secondo;
        }
    };

La classe che è stata appena definita memorizza due valori di un certo tipo. Ad esempio, per dichiarare un oggetto di classe coppia, per memorizzare i due valori interi 115 e 36 si deve richiamare il costruttore in questo modo:

    coppia <int> coordinate(115, 36);

la stessa classe può essere usata per creare un oggetto che memorizzi una coppia di numeri di un qualsiasi altro tipo:

    coppia <double> numeri (3.0, 2.18); 

L'unica funzione membro della classe è stata dichiarata inline, nella stessa dichiarazione della classe. Quando si definisce una funzione membro all'esterno della classe parametrica, bisogna sempre usare il prefisso template <.>:
il file coppia.h

    #include <iostream>
    using namespace std;
    template <class T>
    class coppia {
        T a, b;
      public:
        coppia (T primo, T secondo) {
          a = primo;
          b = secondo;
        }
        T MassimoTra();
    };

Il file coppia.cpp

    
    template <class T>
    T coppia<T>::MassimoTra() {
      T max;
      max = a>b ? a : b;
      return max;
    }

Uso della classe

    int main () {
      coppia <int> coordinate(100, 75);
      cout << "Massimo: " << coordinate.MassimoTra();
      return 0;
    }

Notare la sintassi usata per scrivere la definizione della funzione membro MassimoTra:

    template <class T>
    T coppia<T>::MassimoTra() 

In questa dichiarazione ci sono tre T: La prima è il parametro del template. La seconda T si riferisce al tipo restituito dalla funzione. Anche la terza T (quella tra i segni di minore e maggiore) è richiesta: specifica che questo parametro formale del template della funzione è anche il parametro della classe template.

Specializzazione dei Template

Se si vuole definire una implementazione differente per un template, che viene usata quando si passa uno specificato tipo come parametro del template, si può dichiarare una specializzazione di quel template.

Ad esempio, si consideri una semplice classe chiamata contenitore che può memorizzare un elemento di un tipo qualsiasi, ed ha una funzione membro chiamata incrementa, che ne incrementa il valore. Ci si accorge che quando si memorizza un elemento di tipo char dovrebbe essere più conveniente avere un'implementazione completamente diversa, cioè si preferisce applicare la trasformazione in maiuscolo invece dell'incremento, quindi si decide di dichiarare una classe parametrica specializzata per tale tipo:

    #include <iostream>
    using namespace std;
    template <class T>
    class contenitore {
        T elemento;
      public:
        contenitore (T arg) {
          elemento=arg;
        }
        T incrementa () {
          return ++elemento;
        }
    };

    template <>
    class contenitore <char> {
        char elemento;
      public:
        contenitore (char arg) {
          elemento=arg;
        }
        char maiuscolo () {
          if ((elemento>='a')&&(elemento>='z'))
            elemento+='A'-'a';
          return elemento;
        }
    };
    
    int main () {
      contenitore <int> numero(7);
      contenitore <char> lettera('j');
      cout << numero.incrementa() << endl;
      cout << lettera.maiuscolo() << endl;
      return 0;
    }

Questa è la sintassi usata nella specializzazione della classe parametrica:

    template <> 
    class contenitore <char> { ... };

Notare che il nome della classe parametrica è preceduto da un elenco vuoto di parametri dopo il termine template. Questo è il modo per specificare che si sta definendo un template specializzato.

Ma altrettanto importante da notare è il parametro <char>, della specializzazione della classe, che segue il nome della classe parametrica. Questo parametro stesso individua il tipo che differenzia il comportamento speciale quando si passa un char. Notare la differenza tra la classe parametrica generica e la classe specializzata:

    template <class T> class contenitore { ... };
    template <> class contenitore <char> { ... };

La prima linea è il template della classe generica, la seconda è il template della classe specializzata.

Quando si dichiara una specializzazione per una classe parametrica bisogna definire anche tutti i suoi campi membro, compresi quelli esattamente uguali nella classe parametrica generica, perchè non c'è ereditarietà tra i membri della classe parametrica generica e quelli della classe specializzata.

Parametri senza tipo dei template

A parte i parametri del template che sono preceduti dai termini class o typename, che rappresentano dei tipi, i template possono avere anche parametri di un tipo predefinito. Per un esempio si osservi la seguente classe, usata per contenere sequenze di elementi:

    #include <iostream>
    using namespace std;
    template <class T, int N>
    class sequenza {
        T blocco[N];
      public:
        void scrivi (int x, T value);
        T leggi (int x);
    };
    
    template <class T, int N>
    void sequenza<T, N>::scrivi (int x, T valore) {
      blocco[x] = valore;
    }

    template <class T, int N>
    T sequenza<T, N>::leggi(int x) {
      return blocco[x];
    }

    int main () {
      sequenza <int, 5> valoriInt;
      sequenza <double,5> valoriDouble;
      valoriInt.scrivi (0, 100);
      valoriDouble.scrivi (3, 3.1416);
      cout << valoriInt.leggi(0) << endl;
      cout << valoriDouble.leggi(3) << endl;
      return 0;
    }

Si possono anche impostare valori o tipi di default per le classi parametriche. Ad esempio se la definizione della classe precedente fosse stata:

    template <class T=char, int N=10> class sequenza {..};

Si potrebbero creare oggetti usando i parametri di default del template dichiarando: sequenza<> seq;

Che dovrebbe essere equivalente a: sequenza<char, 10> seq;


Classi contenitore: Vector, List, Map, ...

Un contenitore è un raccoglitore che memorizza una collezione di oggetti (i suoi elementi). I contenitori vengono implementati come classi parametriche, perchè offrono una grande flessibilità nei tipi degli elementi.

Il contenitore si occupa di gestire lo spazio di memoria per i suoi elementi e fornisce le funzioni membro per l'accesso agli elementi, o direttamente oppure tramite oggetti iterator (riferimenti a oggetti, cioè hanno proprietà simili ai puntatori).

I contenitori riproducono, sotto forma di classi, le più comuni strutture dati usate nella programmazione, offrendo accanto ai dati anche le operazioni di accesso: array (vector), code (queue), stack (stack), liste concatenate (list), alberi (set), array associativi (map).

Molti contenitori hanno diverse funzioni membro in comune e ne condividono le funzionalità. La decisione sulla scelta del tipo di contenitore da usare per un caso specifico non dipende solo dalla funzionalità offerta dal contenitore, ma anche dall'efficienza dei membri (complessità).

Definizione di oggetto Iterator

In C++, un iterator è un qualsiasi oggetto che, puntando ad un certo elemento in un range di elementi (come ad esempio un array o un contenitore), riesce ad iterare attraverso gli elementi di quell'insieme, usando alcuni operatori, tra questi l'incremento (++) e il dereferimento (*).

La forma più ovvia di iteratore è il puntatore: Un puntatore può puntare agli elementi di un array, e può scandire tutta la lista dei suoi elementi usando l'operatore incremento (++). Esistono anche altre forme di accesso iterativo. Ogni tipo contenitore (ad esempio vector) ha un tipo iterator specifico, progettato per accedere in modo efficiente agli elementi del contenitore.

Mentre un puntatore è una forma di iteratore, non tutti gli iteratori offrono la stessa funzionalità di un puntatore. Per distinguere i requisiti che dovrà possedere un iteratore usato in uno specifico algoritmo, esistono cinque diverse categorie di iteratori:

classe base Iterator

Questo template della classe base può essere usato per derivare altre classi Iterator. Questa classe base fornisce solo alcuni tipi membro, che infatti non si richiede che siano presenti in qualsiasi tipo di Iterator, ma potrebbero essere utili per derivare l'appropriata classe.

È definito come:

template <class Categoria, class T, class Distance = ptrdiff_t,
          class Pointer = T*, class Reference = T&>

Solo i primi due parametric sono obbligatori, i restanti sono opzionali, se non vengono specificati si usano i tipi di default.

Esempio:

 #include <iostream>
 #include <iterator>
 using namespace std;
1class iteratore : public iterator<input_iterator_tag, int> {
2  int* p;
3public:
4  iteratore(int* x) :p(x) {}
5  iteratore(const iteratore& mit) : p(mit.p) {}
6  iteratore& operator++() {
     ++p;
     return *this;
   }
7  iteratore operator++(int) {
8    iteratore tmp(*this);
9    operator++();
10    return tmp;
   }
11  bool operator==(const iteratore& rhs) {
     return p==rhs.p;
   }
12  bool operator!=(const iteratore& rhs) {
     return p!=rhs.p;
   }
13  int& operator*() {
14    return *p;
   }
 };
 int main () {
15  int numeri[]={10,20,30,40,50};
16  iteratore inizio(numeri);
17  iteratore fine(numeri+5);
18  for (iteratore it(inizio); it!=fine; it++)
19    cout << *it << " ";
   cout << endl;
   system("PAUSE");
   return 0;
 }

Commenti

1la classe iteratore viene derivata dalla classe base iterator, passando i parametri alla classe base, cioè: Categoria: Input, Tipo: int
2la classe possiede il campo membro p di tipo puntatore a intero.
3inizia la sezione pubblica, cioè l'interfaccia della classe.
4il costruttore riceve un puntatore a intero. Dopo l'elenco dei parametri del costruttore c'è un carattere due punti. Questo precede l'elenco dei costruttori della classe base. In questo caso si deve inizializzare il campo membro p della classe derivata. L'inizializzazione avviene in modo implicito: p(x). Cioè per inzializzare p si usa la stessa sintassi di un costruttore. è equivalente a scrivere il costruttore in questo modo: iteratore(int* x) { p = x; }
5viene definito il costruttore copia (per il caso in cui il parametro di ingresso è il riferimento ad un altro oggetto di classe iteratore). Il campo membro viene aggiornato per assumere il valore deil campo membro dell'oggetto ricevuto come parametro.
6viene ridefinito l'operatore di incremento: incrementa il puntatore e ritorna il riferimento all'oggetto che ha eseguito l'incremento.
7viene ridefinito l'operatore di incremento che agisce su un operando di tipo intero:
8viene prima creato un oggetto temporaneo, passando al costruttore il riferimento all'oggetto
9,chiama l'operatore di incremento precedentemente ridefinito restituisce l'oggetto che ha creato.
10Restituisce l'oggetto che ha creato
11ridefinisce l'operatore di confronto
12ridefinisce l'operatore di confronto
13ridefinisce l'operatore *
15dichiara ed inizializza un array di interi.
16crea l'istanza inizio di un oggetto di classe iteratore, inizializzando il campo membro con il riferimento al primo elemento dell'array.
17crea l'istanza fine di un oggetto di classe iteratore, inizializzando il campo membro con il riferimento al 5o elemento dell'array.
18il ciclo for crea l'oggetto temporaneo it, facendolo variare da inizio a fine, con incremento. è qui che vengono richiamati gli operatori riderifiniti: costruttore con parametro, confronto, incremento.
19la stampa richiama l'operatore ridefinito *.

Il template di classe Vector.

#include <iostream>
#include <vector>
using namespace std;
int main () {
  unsigned int i;

Sono consentiti quattro costruttori:

Metodi della classe Vector.