Molti programmi usano strutture dati comuni tipo stack, code, liste. Un programma potrebbe necessitare di una coda di clienti e una coda di messaggi. La soluzione ovvia è scrivere la coda per i clienti e fare copia e incolla del codice esistente per creare la coda di messaggi, ma se poi ad un certo punto del programma si deve implementare una coda di ordini? Di nuovo copia, incolla, trova, sostituisci… il lavoro inizia a diventare ripetitivo e confusionario. Se poi ci si accorge che l’implementazione della coda necessita di alcune modifiche? Ormai è stata implementata in più parti del programma, il compito diventa sempre più arduo e fastidioso oltre che sempre più facilmente soggetto ad errori. Reinventare il codice non è un approccio intelligente in un ambiente ad oggetti la cui filosofia principale consiste nello scrivere codice il più riusabile possibile. Risulta molto più semplice e sensato creare una unica implementazione generica della coda e di volta in volta assegnare ad essa un tipo arbitrario. Come fare? In C++ la risposta è parametrizzazione dei tipi, comunemente riferita come “templates”.

I templates C++ permettono infatti di creare un template generico Queue<T> che prende come parametro un tipo T. T può essere sostituito con tipi reali, ad esempio Queue<Clienti>; sarà compito del compilatore C++ generare la classe Queue<Clienti>. Cambiare l’implementazione della coda diventa un’operazione relativamente semplice dato che i cambiamenti vanno applicati solo al template Queue<T> e automaticamente vengono riflessi sulle classi Queue<Clienti>, Queue<Messaggi>, Queue<Ordini>.

C++ fornisce due tipi di templates: per classe e per funzione. Creare una funzione template vuol dire scrivere una funzione generica che può essere usata con tipi arbitrari. Gli algoritmi della Standard Template Library (STL) sono implementati all’interno di funzioni templates e il contenitore è implementato come una classe template.

Classi Template

Una definizione di una classe template è molto simile alla definizione di una normale classe, eccetto per il fatto che contiene come prefisso la parola chiave template. Per esempio, la seguente è una definizione di classe template per uno Stack.

template <class T>
class Stack
{
public:
  Stack(int = 10);
  ~Stack() { delete stackPtr; }
  int push(const T&);
  int pop(T&);
  int isEmpty() const { return top == -1; }
  int isFull() const { return top == (size - 1); }
private:
  int size; //Numero di elementi nello stack
  int top;
  T* stackPtr;
};

Come facilmente si nota, il tipo generico T è sostituito al tipo specializzato che altrimenti avremmo dovuto specificare in una normale classe. T può dunque essere di qualsiasi tipo, per esempio Stack<Token> definisce uno Stack di tipo Token, dove Token è una classe definita dall’utente; Stack<int> definisce uno Stack di tipo nativo int.

A questo punto bisogna implementare le funzioni membro della classe; precisiamo subito che nonostante la relativa semplicità di implementazione di una classe template, dovuta anche e soprattutto all’alta somiglianza con l’implementazione di una normale classe, dal punto di vista del compilatore la situazione è un pò differente; il compilatore infatti ha bisogno sia delle dichiarazioni che delle definizioni ogni volta che il template viene istanziato; per questo motivo, per non incorrere in errori apparentemente inspiegabili è buona norma utilizzare lo stesso file sia per le dichiarazioni che per le definizioni.

Le definizioni, così come le classi contengono come prefisso la parola chiave template. Segue l’implementazione delle funzioni della classe Stack.

template <class T>
Stack<T>::Stack(int s)
{
  size = (s > 0 && s < 10000) s : 10;
  top = -1;
  stackPtr = new T[size];
}

template <class T>
int Stack<T>::push(const T& item)
{
  if (!isFull()) {
    stackPtr[++top] = item;
    return 1;
  }
  return 0;
}

template <class T>
int Stack<T>::pop(const T& val)
{
  if (!isEmpty()) {
    val = stackPtr[top--];
    return 1;
  }
  return 0;
}

Usare una classe template è molto semplice. Si crea la classe specifica passando il tipo richiesto come parametro tipo del template. Il processo è comunemente conosciuto come “Istanziare una classe”. Segue il codice di esempio per utilizzare il sopra esposto template Stack.

#include <iostream>
#include "Stack.h"
using namespace std;

typedef Stack<int> IntStack;

int main(void) {
  IntStack is(5);
  int i = 0;
  cout << "Inserimento elementi nello Stack is" << endl;
  while (is.push(i++))
    cout << i << ' ';
  cout << endl << "Stack Pieno" << endl;
  cout << "Estrazione elementi dallo Stack is" << endl;
  while (is.pop(i))
    cout << i << ' ';
  cout << "Stack Vuoto" << endl;
}

Si può notare come all’inizio del programma è stato usato un typedef per dare un nuovo nome allo stack di interi Stack<int>; è infatti una buona pratica di programmazione usare i typedef quando si istanzia una classe template perchè porta diversi vantaggi in piu’: maggiore chiarezza nel codice, maggiore mnemonicità, e in caso di modifiche alla definizione del template, modifiche praticamente nulle al codice che lo utilizza.

Funzioni Template

Per eseguire operazioni identiche per tipi diversi di dati in modo compatto e conveniente, come detto in precedenza, si ricorre alle funzioni template. In base al tipo di argomento il compilatore genera automaticamente funzioni separate, appropriate al tipo specificato.

Come per le classi, le funzioni templati sono implementate allo stesso modo di funzioni regolari, anche questa volta l’unica differenza riguarda il tipo e la parola chiave template. Per esempio:

#include <iostream>
using namespace std;

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

E’ possibile chiamare la funzione max() con un tipo arbitrario, il compilatore genererà automaticamente una funzione specializzata per quel tipo. Esempio:

void main()
{
  cout << "max(100,150) = " << max(100, 150) << endl;
  cout << "max('p', 'q') = " << max('p', 'q') << endl;
  cout << "max(3.14, 6.28) = " << max(3.14, 6.28) << endl;
}

Output:

 max(100, 150) = 150
 max('p', 'q') = q
 max(3.14, 6.28) = 6.28

Manca ancora qualcosa… se avessimo chiamato max() con due stringhe come parametri, il compilatore avrebbe interpretato l’argomento come char *, ed avrebbe fatto bene; il problema si sarebbe verificato però all’interno nostra funzione che non è progettata per gestire stringhe come argomenti. Per ovviare al problema la soluzione viene chiamata Specializzazione delle funzioni template, si interviene cioè a specializzare una funzione template per soddisfare casi specifici i cui dati richiedono un trattamento diverso da quello standard.

Il nostro codice diventa dunque:

#include <iostream>
#include <cstring>
using namespace std;

//Classe template max()
template <class T>
T max(T a, T b)
{
  return a > b ? a : b;
}

//Specializzazione di max() per trattare le stringhe
template <>
const char *max(const char* str1, const char* str2)
{
  return (strcmp(str1, str2) > 0 ) str1 : str2;
}

void main()
{
  cout << "max(100,150) = " << max(100, 150) << endl;
  cout << "max('p', 'q') = " << max('p', 'q') << endl;
  cout << "max(3.14, 6.28) = " << max(3.14, 6.28) << endl;
  cout << "max(\"Mele\", \"Pere\") = " << max("Mele", "Pere") << endl;
}

L’Output sarà:

 max(100, 150) = 150
 max('p', 'q') = q
 max(3.14, 6.28) = 6.28
 max("Mele", "Pere") = Pere

Membri e variabili statiche

Un’ultima cosa da sapere prima di iniziare ad usare i templates riguarda le variabili e i membri statici.

Bisogna tener presente infatti che ogni classe generata da una classe template ha le proprie copie di variabili e membri statici, così come ogni funzione generata da una funzione template ha le proprie variabili statiche al suo interno.

template <class T>
class X {
 public:
   static T s;
};

Ogni volta che la verrà istanziata una classe con un tipo arbitrario, ci sarà una variabile statica s di tipo specificato;

Per questo motivo bisogna integrare la definizione di classe template con il valore che la variabile s assumerà a seconda del tipo che andremo ad usare:

template <class T> T X<T>::s = 0;  //Per default &egrave; 0
template <> int X<int>::s = 3;     //Per il tipo int il valore &egrave; 3
template <> char* X<char*>::s = "Ciao";  //Per il tipo char * il valore è "Ciao"

Lo stesso vale a dire per le funzioni template: ogni istanza di quella funzione conterrà le proprie variabili statiche del tipo specificato.