next up previous contents index
Next: Un esempio concreto: lista Up: Dettagli sulle classi. Templates Previous: Un esempio concreto: stringhe   Indice   Indice analitico

Templates

La programmazione in C++ consiste essenzialmente nella creazione di classi, le quali poi siano legate le une alle altre con le tecniche di composizione e derivazione, di cui parleremo nella parte finale del nostro corso. Un particolare tipo di classe è il container (it. ``contenitore''): una classe avente una struttura dati adatta a contenere oggetti di altro tipo; ad esempio un array o una lista sono contenitori. Sorge però il seguente problema: la struttura di una classe contenitore è probabilmente indipendente dal tipo di oggetti che essa dovrà contenere, tuttavia il compilatore deve sapere sempre con quale tipo di dati stiamo lavorando, essendo il C++ un linguaggio a tipizzazione stretta. Ci sono due strade da seguire. La prima consiste nel creare un contenitore adatto a tutti i tipi che incontreremo nel nostro progetto, presupponendo dunque di avere pianificato ogni cosa nella fase preliminare di programmazione, il che è praticamente impossibile; la seconda, decisamente migliore, consiste nell'utilizzare i template.

Un template (it. ``modello'') è una classe avente alcuni ``gradi di libertà'': con la seguente premessa ad una dichiarazione di classe

template <class Tipo_1, class Tipo_2, ...>
possiamo utilizzare all'interno della nostra classe i tipi Tipo_1, Tipo_2 elencati tra parentesi angolate nella dichiarazione di template senza sapere in fase di progettazione che tipi essi siano in realtà.

Programmiamo ad esempio una semplice classe array, utilizzando la tecnica dei template.


// ex10_5_1.h
// esempio di array come classe template

#include <iostream.h>

template<class Tipo>
class Array {
  friend ostream& operator<< (ostream& os, const Array& v) {
    for (int i = 0; i < v.nelem; i++)
      if (i % 5 == 4)
        os << v.array[i] << "\n";
      else
        os << v.array[i] << "\t";
    return os;
  }
public:
  Array (unsigned n) {
    nelem = n;
    array = new Tipo[nelem]; }
  Tipo& operator[] (int i) { 
    return array[i]; }
  ~Array () { delete[] array; }
private:
  // dichiariamo privati il costruttore di copia e
  // l'operatore di assegnamento: possiamo in questa
  // maniera inibire il loro utilizzo da parte dell'utente
  Array (const Array&);
  const Array& operator= (const Array&);
  Tipo* array;
  unsigned nelem;   // numero di celle dell'array
};
Notiamo prima di tutto che le classi template devono essere dichiarate interamente all'interno di un file di intestazione: esse non possono essere compilate perché non sappiamo a priori i tipi di alcuni dei dati trattati nella classe, nel nostro esempio il tipo degli elementi dell'array. Consideriamo il seguente programma di esempio, che utilizza il nostro Array:

// ex10_5_1.cpp
#include "ex10_5_1.h"
void main() {
  Array<int> a(4);
  Array<double> b(3);
  a[0] = 3; a[1] = 2; a[2] = -3; a[3] = 6;
  b[0] = 3.14; b[1] = 1.0; b[2] = 2.71;
  cout << a << "\n" << b << "\n";
}

output:
 3 2 -3 6

 3.14 1 2.71

Quando il compilatore incontra la dichiarazione Array<int> a esso genera una copia della classe Array sostituendo Tipo con int, e le assegna un nome formato dalla combinazione di Array e int, ad esempio (non è rilevante) __Array_int; successivamente compila la classe __Array_int, la quale non ha tipi di dati incogniti. Quando dichiariamo un array di double tramite Array<double> b, il compilatore effettua il medesimo lavoro appena descritto per l'array di interi creando e compilando la classe, ad esempio, __Array_double. Naturalmente possiamo dichiarare template anche con tipi non primitivi, ad esempio con classi esistenti, come vediamo nel seguente (il quale fa riferimento alla classe Complesso contenuta negli omonimi files):


// ex10_5_2.cpp

#include "ex10_5_1.h"   // contiene il template Array
#include "complesso.h"  // contiene la classe Complesso
#include <time.h>
#include <stdlib.h>

ostream& operator<< (ostream& os, const Complesso& c) {
  os << "( " << c.reale() << ", " << c.immaginaria() << ")";
  return os;
}

void main() {
  srand ( time(0) );
  Array<Complesso> a(15);
  for (int i = 0; i < 15; i++) {
    double x = rand() % 20 - 10;
    double y = rand() % 20 - 10;
    a[i] = Complesso (x, y);
  }
  cout << a << "\n";
}

esempio di output:
 ( 8, -5) ( -3, 2) ( 3, 0) ( -2, -7) ( -8, -9)
 ( -7, -8) ( 6, -1) ( 4, 0) ( -10, -7) ( -4, 0)
 ( -4, -10) ( -10, -3) ( 9, 9) ( 2, 1) ( 1, -9)

I template presentano il problema di non essere adatti a qualunque tipo di dati: ad esempio in ex10_5_2.cpp abbiamo dovuto sovrapporre l'operatore di uscita per un numero complesso, per adattare il tipo di dati al contenitore. In altri casi non è possibile fare in modo che un template funzioni con un determinato tipo; si consideri ad esempio il costruttore di Array: quando creiamo un array di Type, supponiamo che tale tipo abbia un costruttore default, il che non è vero per ogni tipo di dati; nel caso Type non ha un costruttore default, esso non potrà mai essere utilizzato con il nostro contenitore. Un altro tipico problema che si presenta con i contenitore riguarda l'ordinamento dei dati, che presuppone l'esistenza di un operatore di confronto nel tipo contenuto; siccome quasi tutti i container contengono una procedura di ordinamento, è buona norma in fase di costruzione di una classe sovrapporre sempre gli operatori di confronto (di solito è sufficiente il <).

Vediamo un semplice esempio di template con due tipi di dati incogniti:


// ex10_5_3.cpp

#include <iostream.h>

template<class T1, class T2>
class DoppioArray {
  friend ostream& operator<< (ostream& os, const DoppioArray& d) {
    for (int i = 0; i < d.dim; i++) {
      if (d.array1[i] != 0)
        os << *(d.array1[i]) << "\t";
      else
        os << "(vuoto)\t";
      if (d.array2[i] != 0)
        os << *(d.array2[i]) << "\n";
      else
        os << "(vuoto)\n";
    }
    return os; }
public:
  DoppioArray (unsigned n);
  ~DoppioArray ();
  // funzioni di accesso
  unsigned dimensione() const { return dim; }
  // aggiunge un oggetto
  T1* aggiungi1 (unsigned pos, const T1&);
  T2* aggiungi2 (unsigned pos, const T2&);
  // elimina un oggetto
  void elimina1 (unsigned pos);
  void elimina2 (unsigned pos);
private:
  // costruttore di copia e operatore di assegnamento
  DoppioArray (const DoppioArray&);
  const DoppioArray& operator= (const DoppioArray&);
  // struttura dati
  T1** array1;
  T2** array2;
  unsigned dim; // dimensione dell'array
};

template<class T1, class T2>
DoppioArray<T1,T2>::DoppioArray (unsigned n) {
  dim = n;
  array1 = new T1*[n];
  array2 = new T2*[n];
  // inizializza l'array con puntatori nulli
  for (int i = 0; i < dim; i++) {
    array1[i] = 0;
    array2[i] = 0;
  }
}

template<class T1, class T2>
DoppioArray<T1,T2>::~DoppioArray () {
  for (int i = 0; i < dim; i++) {
    delete array1[i];
    delete array2[i];
  }
  delete[] array1;
  delete[] array2;
}

template<class T1, class T2>
T1* DoppioArray<T1,T2>::aggiungi1 (unsigned pos, const T1& oggetto) {
  if (pos <= 0 || pos > dim)
    return 0; // inserimento non riuscito
  // elimina l'oggetto preesistente in posizione pos
  delete array1[pos - 1];
  array1[pos - 1] = new T1(oggetto);
  return array1[pos - 1];
}

template<class T1, class T2>
T2* DoppioArray<T1,T2>::aggiungi2 (unsigned pos, const T2& oggetto) {
  if (pos <= 0 || pos > dim)
    return 0; // inserimento non riuscito
  // elimina l'oggetto preesistente in posizione pos
  delete array2[pos - 1];
  array2[pos - 1] = new T2(oggetto);
  return array2[pos - 1];
}

template<class T1, class T2>
void DoppioArray<T1,T2>::elimina1 (unsigned pos) {
  if (pos <= 0 || pos > dim)
    return; // eliminazione non riuscita
  // elimina l'oggetto
  delete array1[pos - 1];
  array1[pos - 1] = 0;
}

template<class T1, class T2>
void DoppioArray<T1,T2>::elimina2 (unsigned pos) {
  if (pos <= 0 || pos > dim)
    return; // eliminazione non riuscita
  // elimina l'oggetto
  delete array2[pos - 1];
  array2[pos - 1] = 0;
}

void main() {
  DoppioArray<int, double> d(5);
  d.aggiungi1(1, 5);
  d.aggiungi1(3, -2);
  d.aggiungi2(3, 3.14);
  d.aggiungi2(4, 2.71);
  d.elimina2 (4);
  d.aggiungi2(5, .5);
  cout << d << "\n";
}

output:
 5 (vuoto)
 (vuoto) (vuoto)
 -2 3.14
 (vuoto) (vuoto)
 (vuoto) 0.5

La struttura dati di DoppioArray è costituita , come l'identificatore della classe suggerisce, da due array: uno di tipo T1, l'altro di tipo T2. Come è evidente, l'utilizzo di due tipi di dati incogniti in un template è del tutto simile a quello di un singolo tipo.

All'interno di un template è possibile avere non solo tipi incogniti, ma anche valori di un certo tipo; mostriamo tale possibilità nel prossimo esempio, contenente una classe array rappresentata con un array non dinamico.


// ex10_5_4.cpp

#include <iostream.h>

// indichiamo con N il numero di elementi dell'array
// NOTA: N e` noto a tempo di COMPILAZIONE
// Tipo DEVE avere un costruttore default
template<class Tipo, unsigned int N>
class ArrayStatico {
public:
  ArrayStatico () { }
  // il costruttore di copia, l'operatore di assegnamento
  // e il distruttore NON sono necessari in quanto
  // tutti i membri sono non dinamici
  // ArrayStatico (const ArrayStatico&);
  // const ArrayStatico& operator= (const ArrayStatico&);
  // ~ArrayStatico ();
  unsigned int n_elementi () const { return N; }
  Tipo& operator[] (int indice) const { return array[indice]; }
private:
  Tipo array[N];
};

template<class Tipo, int N>
ostream& operator<< (ostream& os, const ArrayStatico<Tipo, N> v) {
  for (int i = 0; i < v.n_elementi(); i++)
    os << v[i] << "\n";
  return os;
}

void main() {
  ArrayStatico<int, 5> v;
  for (int i = 0; i < 5; i++)
    v[i] = 5 - i;
  cout << v << "\n";
}

Utilizzando i template è importante ricordare che sia i tipi che i valori incogniti sono noti a tempo di compilazione, per cui è possibile farne uso come se essi fossero già noti a priori, a tempo di programmazione della classe; è per questo motivo che è possibile creare un array non dinamico in una classe: N è considerata dal compilatore a tutti gli effetti una costante di tipo unsigned int.

ex-2
si programmi una classe rappresentante una matrice di N $\times$ M elementi di tipo Tipo, ove N, M e Tipo siano due valori ed un tipo incogniti in un template;


next up previous contents index
Next: Un esempio concreto: lista Up: Dettagli sulle classi. Templates Previous: Un esempio concreto: stringhe   Indice   Indice analitico
Claudio Cicconetti
2000-09-06