Dal C al C++/Utilizzo basilare di librerie/L'uso dei template

Indice del libro

In C++ si possono definire non solo classi e funzioni, ma anche template di classe e template di funzione.

Per esempio, nella libreria standard è definito il template di classe "vector", che è un array dinamico. Tale template di classe non definisce il tipo degli elementi contenuti nell'array. Tale tipo è un parametro del template. Per poter utilizzare il template si deve specificare il tipo degli elementi. Vediamo qualche esempio:

vector a; // Illegale, parametro non specificato
vector<int> b; // Array dinamico di int
vector<int> c; // Altro array dinamico di int
b = c; // Copia un vector su un altro dello stesso tipo
vector<double> d; // Array dinamico di double
b = d; // Illegale, non si possono copiare array di double su array di int

La parola "vector" rappresenta la definizione di una classe generica, definita a meno di un parametro; non è una classe, ma una famiglia parametrica di classi, e quindi non si può usare direttamente per dichiarare o definire una variabile o una funzione.

Se dopo la parola "vector" si pone tra parentesi angolari un'espressione che identifica un tipo, si ottiene un'istanza del template, che è un tipo, utilizzabile come qualunque altro tipo.

L'istanziazione di un template di classe, cioè il passaggio dal template al tipo, viene eseguito in fase di compilazione, e quindi in fase di esecuzione non esistono affatto i template.

Un template può richiedere come parametro un valore intero, invece di un tipo. Consideriamo il seguente programma:

   #include <iostream>
   #include <bitset>
   using namespace std;
   int main() {
      bitset<6> a;
      a[0] = 1;
      a[2] = 1;
      cout << a;
   }

La prima riga della funzione "main" è la dichiarazione della variabile "a", di tipo "bitset<6>". Il template di classe "bitset" fa parte della libreria standard, ed è definito nel file di intestazione "bitset".

Questo template di classe rappresenta una sequenza di bit di lunghezza fissa, e richiede un parametro di tipo intero che indica il numero di bit contenuti nell'oggetto.

Nel nostro esempio, "a" è una sequenza di sei bit. L'oggetto "a" non è inizializzato esplicitamente, ma la classe bitset<6> lo inizializza implicitamente impostando a zero tutti i suoi bit.

Le due istruzioni successive impostano a 1 il primo e il terzo bit, contando da zero, cioè quelli di indice zero e di indice due.

L'ultima istruzione emette sulla console una rappresentazione testuale della variabile. Il risultato sarà la stampa della stringa "000101".

Chiaramente non si possono mescolare tipi con valori interi. Nella definizione di "vector" è specificato che il parametro deve essere un tipo, mentre nella definizione di "bitset" è specificato che il parametro deve essere un intero. Le seguenti due righe producono errori di compilazione:

vector<4> a; // Illegale, vector richiede un tipo
bitset<int> b; // Illegale, bitset richiede un valore intero

Siccome l'istanziazione dei template avviene in fase di compilazione, è necessario che il valore intero usato come parametro di un template sia una costante calcolabile dal compilatore. Per esempio:

 bitset<3 * 2 + 7> b1; // Valido
 const int k1 = 3;
 const int k2 = 2;
 const int k3 = 7;
 bitset<k1 * k2 + k3> b2; // Valido
 int c3 = 7;
 bitset<k1 * k2 + c3> b3; // Illegale

L'espressione usata per "b1" è ovviamente costante.

L'espressione usata per "b2" usa solo variabili "const", quindi il compilatore è in grado di valutare tale espressione.

L'espressione usata per "b3" somiglia alla precedente, ma usa la variabile "c3", che pur avendo già un valore in quel punto, è una variabile non "const" e quindi il suo valore non è costante, e il compilatore si rifiuta di usare tale valore contingente per istanziare un template.

In C++, oltre ai template di classe esistono i template di funzione. Ecco un esempio:

   #include <iostream>
   #include <string>
   using namespace std;
   int main() {
      double a = max<double>(2.3, 6.8);
      string b = min<string>(string("abc"), string("xyz"));
      cout << a << " " << b;
   }

Questo codice utilizza due template di funzione: "max", che rende il valore maggiore tra i suoi due parametri, e "min", che rende il valore minore tra i suoi due parametri.

Il programma stampa su console la stringa "6.8 abc"; infatti, il numero "6.8" è maggiore del numero "2.3", e la stringa "abc" è minore della stringa "xyz" in ordine alfabetico.

In modo analogo ai template di classe, anche questi template di funzione sono stati istanziati specificando tra parentesi angolari il parametro del template, che per queste funzioni è il tipo dei parametri della funzione. Se si specifica un tipo e si passa un tipo diverso, si ottiene un errore sintattico. Per esempio:

   double a = max<double>(string("abc"), string("xyz")); // Illegale
   string b = min<string>(2.3, 6.8); // Illegale

Nella prima riga si passano due stringhe alla funzione "max<double>", che si attende due numeri; nella seconda riga si passano due numeri alla funzione "min<string>", che si attende due stringhe.

A differenza dei template di classe, i template di funzione possono sottintendere il parametro del template. Per esempio, il seguente programma è equivalente al precedente:

   #include <iostream>
   #include <string>
   using namespace std;
   int main() {
      double a = max(2.3, 6.8);
      string b = min("abc", "xyz");
      cout << a << " " << b;
   }

Siccome al template di funzione "max" vengono passati come parametri di funzione due valori entrambi di tipo "double", il compilatore deduce che il tipo del template è "double", e istanzia la funzione "max<double>". Analogamente, siccome al template di funzione "min" vengono passati come parametri di funzione due valori entrambi di tipo "string", il compilatore deduce che il tipo del template è "string", e istanzia la funzione "min<string>".

Tale inferenza automatica permette spesso al programmatore di scrivere meno codice. Tuttavia a volte l'inferenza non è possibile, in quanto c'è un'ambiguità. Per esempio:

    double a = min(2, 3.4); // Illegale

In questa riga il compilatore non sa se istanziare la funzione "min<double>" o "min<int>" e quindi si arrende.

L'uso che viene fatto del valore reso da una funzione non viene mai utilizzato per inferire il parametro di un template, quindi il fatto che il valore sia usato per inizializzare una variabile di tipo "double" è ininfluente. Per risolvere l'ambiguità si può usare una delle tre righe seguenti:

   double a = min<double>(2, 3.4);
   double a = min(2., 3.4);
   double a = min((double)2, 3.4);

La prima versione specifica esplicitamente il parametro del template, generando una funzione che si attende due valori double. A questo punto, il parametro "2" viene automaticamente convertito in "double", come avviene quando in linguaggio C si passa un valore intero a una funzione che si attende un parametro di tipo "double".

Le altre due versioni convertono il valore "2" nel corrispondente valore "double".