Le funzioni ricevono parametri di un certo tipo e restituiscono un risultato di un tipo ben preciso.
I template consentono di operare con tipi generici. Con i template, anzichè ripetere il codice di una funzione per ogni nuovo tipo di dato, si possono creare funzioni in grado di usare lo stesso codice per tipi di dati diversi. Per esempio:
int somma(const int x, const int y) {
return x + y;
}
Affinchè questa funzione operi con i dati di tipo double, deve essere modificata nel modo seguente:
double somma (const double x, const double y) {
return x + y;
}
Per una funzione così semplice, non è una grossa fatica scrivere le poche modifiche indicate, ma se non si vuole ripetere l'intestazione della funzione, per ogni tipo, bisogna scrivere un template. Le funzioni template hanno lo scopo di fornire risultati corretti indipendentemente dal tipo dei parametri.
La sintassi per dichiarare una funzione template è:
template <class identificatoreClasse> dichiarazione_di_funzione;
oppure
template <typename identificatoreDiTipo> dichiarazione_di_funzione;
Queste due forme sono equivalenti. Il parametro del template rappresenta un tipo che non è stato specificato nella dichiarazione, ma verrà esplicitato in fase di chiamata della funzione. Gli identificatori di tipo usati nella dichiarazione verranno sostituiti con i tipi specificati nella chiamata della funzione, allo stesso modo in cui vengono passati i parametri effettivi ad una funzione. La funzione somma può essere scritta in questa forma:
template <typename T>
T somma(const T a, const T b) {
return a + b;
}
Quando viene richiamata la funzione somma, con un particolare tipo di parametri, verranno sostituite tutte le T nel codice della funzione. La sintassi per richiamare una funzione template è:
nome_funzione <tipo> (parametri);
La seguente funzione richiama la funzione template somma, definita sopra:
int main() {
cout << somma <int> (1, 2) << endl;
cout << somma <float> (1.21, 2.43) << endl;
return 0;
}
Questo programma stampa i valori 3 e 3.64 su linee separate.
L'identificatore può essere usato in qualsiasi modo all'interno della funzione template, purchè la sostituzione dell'identificatore con il tipo produca un codice corretto.
Una funzione template può essere richiamata anche senza specificare un tipo per i parametri. Questo si può fare nei casi in cui il compilatore è in grado di determinare il tipo del parametro effettivo risalendo alla dichiarazione delle variabili oppure deducendolo dal modo in cui viene scritto il valore. ad esempio 1 è un intero mentre 1.0 è un double.
Nell'esempio precedente si sarebbe potuto anche scrivere quanto segue:
int main() {
cout << somma(1, 2) << endl;
cout << somma(1.21, 2.43) << endl;
return 0;
}
I Templates possono specificare più di un tipo di parametro. Ad esempio:
#include <iostream>
using namespace std;
template <typename T, typename U>
U somma(const T a, const U b) {
return a + b;
}
int main() {
cout << somma <int, float> (1, 2.5) << endl;
return 0;
}
Questo programma produce la stampa 3.5. Il primo parametro del template (T) viene sostituito dal primo parametro usato nella chiamata (int), il secondo parametro del template (U) viene sostituito dal secondo parametro specificato nella chiamata (float).
In questo caso la funzione può essere richiamata scrivendo: somma(1, 2.5), cioè senza specificare i tipi, perchè il compilatore è in grado di determinare che 1 è un intero e 2.5 è un float.
Si possono costruire anche classi template, allo scopo di dichiarare alcuni campi membro della classe del tipo generico:
#include <iostream>
using namespace std;
template <class T>
class Punto {
private:
T x, y;
public:
Punto(const T u, const T v) {
x = u;
y = v;
}
T leggiX() { return x; }
T leggiY() { return y; }
};
int main() {
Punto <float> fPunto(2.5, 3.5);
cout << fPunto.leggiX() << ", " << fPunto.leggiY() << endl;
return 0;
}
Questo programma stampa 2.5, 3.5.
Nel template della classe Punto le funzioni membro sono state definite inline. Quando una funzione membro si definisce esternamente alla classe, si deve far precedere la definizione dal prefisso template <…>, rispettando la seguente sintassi:
template <typename T>
T nomeClasse<T>::NomeFunzione()
Quindi, ad esempio, la funzione leggiX, Il cui prototipo (T leggiX();) deve comunque essere presente all'interno della definizione della classe, potrebbe essere dichiarata esternamente alla classe nel modo seguente:
template <typename T>
T Punto <T>::leggiX() { return x; }
La prima T è il parametro del template. La seconda T si riferisce al tipo restituito dalla funzione. La terza T (tra i segni < e >) serve a specificare che il parametro del template della funzione è anche il parametro del template della classe.
Si possono anche definire implementazioni differenti di uno stesso template usando la specializzazione dei template. In altri termini in corrispondenza di un certo tipo cambia il codice da eseguire.
Si consideri il seguente esempio, in cui si dichiara un classe chiamata Contenitore che memorizza un elemento di un certo tipo. La classe possiede una funzione membro che ne incrementa il valore, ma se l'elemento da memorizzare è un carattere si desidera rappresentare il valore in maiuscolo:
#include <iostream>
#include <cctype>
using namespace std;
template <typename T>
class Contenitore {
private:
T elem;
public:
Contenitore(const T arg) {
elem = arg
}
T inc() { return elem+1; }
};
template <>
class Contenitore <char> {
private:
char elem;
public:
Contenitore(const char arg) {
elem = arg
}
char maiuscolo() { return toupper(elem); }
};
int main() {
Contenitore <int> icont(5);
Contenitore <char> ccont('r');
cout << icont.inc() << endl;
cout << ccont.uppercase() << endl;
return 0;
}
Questo programma stampa 6, passa alla riga successiva, e stampa R. La classe Contenitore possiede due implementazioni:
una generica ed un'altra specificamente predisposta per un tipo char. Si noti la sintassi
template <>
class Contenitore <char> { … }
usata per dichiarare una specializzazione. La prima riga, in cui la lista dei parametri del template è vuota, ha lo scopo di specificare che si sta dichiarando una classe template. Dopo il nome della classe viene specificato il tipo particolare (char) che ha lo scopo di specializzare la classe.
Si confronti la sintassi delle due dichiarazioni:
template <class T> class Contenitore { … }; (template generico)
template <> class Contenitore <char> { … }; (template specializzato)
È possibile parametrizzare i template:
#include <iostream>
using namespace std;
template <typename T, int N>
class Vettore {
private:
T elem[N];
public:
T scrivi(const int i, const T valore) { elem[i] = valore; }
T leggi(const int i) { return elem[i]; }
};
int main() {
Vettore <int, 5> intVet;
Vettore <float, 10> floatVet;
intVet.scrivi(2, 3);
floatVet.scrivi(3, 3.5);
cout << intVet.leggi(2) << endl;
cout << floatVet.leggi(3) << endl;
return 0;
}
Questo programma stampa 3, passa sulla riga successiva e stampa 3.5. Una istanza della classe Vettore lavora su un array di 5 elementi interi mentre l'altra istanza lavora su un array di 10 elementi float.
I valori di default possono essere impostati tra i parametri del template. Per esempio la precedente definizione del template potrebbe essere:
template <typename T=int, int N=5> class Vettore { … }
e si sarebbe potuto creare un Vettore usando i parametri di default scrivendo:
Vettore<> identificatore;
La libreria C++ Standard Template Library (STL) contiene molte classi contenitore e molti algoritmi. Queste librerie sono state scritte usando i template e quindi sono di tipo generiche. I contenitori disponibili in STL sono liste, mappe, code, insiemi, stack, e vettori. Gli algoritmi, invece, comprendono l'accesso sequenziale, l'ordinamento, la ricerca, la fusione, la gestione della memoria libera e operazioni di ricerca minimo/massimo.
Di seguito verranno esaminati alcuni di questi algoritmi, tramite esempi.
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
int main() {
set <int> iset;
iset.insert(5);
iset.insert(9);
iset.insert(1);
iset.insert(8);
iset.insert(3);
cout << "l'insieme iset contiene:";
set <int>::iterator it;
for(it=iset.begin(); it != iset.end(); it++)
cout << " " << *it;
cout << endl;
int cerca;
cin >> cerca;
if(binary_search(iset.begin(), iset.end(), cerca))
cout << "Trovato " << cerca << endl;
else
cout << "Non ho trovato " << cerca << endl;
return 0;
}
Nell'esempio proposto viene creato un insieme e vengono inseriti alcuni interi in esso. Poi viene creato un iteratore. Un iteratore è un puntatore che permette di accedere con un ciclo agli elementi dell'insieme.
Quasi tutte le classi contenitore forniscono un iteratore.
Usando questo iteratore si crea un ciclo per accedere in successione a tutti gli elementi dell'insieme e si stampa: iset contiene: 1 3 5 8 9. Si noti che l'insieme ordina automaticamente i suoi elementi. Nella parte finale del programma viene chiesto all'utente di fornire un valore da cercare, dopo di che inizia la ricerca e viene comunicato l'esito.
Ecco un altro esempio
#include <iostream>
#include <algorithm>
using namespace std;
void stampaVet(const int arr[], const int len) {
for(int i=0; i < len; i++)
cout << " " << arr[i];
cout << endl;
}
int main() {
int a[] = {5, 7, 2, 1, 4, 3, 6};
sort(a, a+7);
stampaVet(a, 7);
rotate(a,a+3,a+7);
stampaVet(a, 7);
reverse(a, a+7);
stampaVet(a, 7);
return 0;
}
Questo programma stampa le righe seguenti:
1 2 3 4 5 6 7
4 5 6 7 1 2 3
3 2 1 7 6 5 4
Per ulteriori informazioni e riferimenti sulle classi contenitore e gli algoritmi della STL consultare i seguenti link: http://www.cplusplus.com/reference/stl e http://www.cplusplus.com/reference/algorithm/.
Si consideri la seguente struttura:
struct Moneta {
int euro;
int cent;
};
Si potrebbe sentire l'esigenza di sommare due oggetti Moneta e ottenere un nuovo risultato, proprio come in una normale addizione:
Moneta a = {2, 50};
Moneta b = {1, 75};
Moneta c = a + b;
Il compilatore produce un messaggio di errore, ma si può ridefinire il comportamento del compilatore quando si trova di fronte a situazioni simili a quella proposta (l'addizione di due oggetti)
La ridefinizione dell'operatore di addizione si può fare all'interno della classe:
Moneta operator+(const Moneta o) {
Moneta tmp = {0, 0};
tmp.cent = cent + o.cent;
tmp.euro = euro + o.euro;
if(tmp.cent >= 100) {
tmp.euro += 1;
tmp.cent -= 100;
}
return tmp;
}
Oppure all'esterno della classe come una funzione indipendente dalla classe:
Moneta operator+(const Moneta m, const Moneta o) {
Moneta tmp = {0, 0};
tmp.cent = m.cent + o.cent;
tmp.euro = m.euro + o.euro;
if(tmp.cent >= 100) {
tmp.euro += 1;
tmp.cent -= 100;
}
return tmp;
}
Analogamente si può ridefinire l'operatore di scorrimento: << per mostrare il risultato:
ostream& operator<<(ostream &output, const Moneta &o)
{
output << "E" << o.euro << "." << o.cent;
return output;
}
Assumendo di aver usato le definizioni precedenti si esegua il seguente programma:
int main() {
Moneta a = {2, 50};
Moneta b = {1, 75};
Moneta c = a + b;
cout << c << endl;
return 0;
}
si ottiene la stampa: E4.25.
I possibili operatori ridefinibili sono:
+ | - | * | / | += | -= | *= | /= | % | %= | ++ | - - |
= | == | < | > | <= | >= | ! | != | && | || | ||
<< | >> | <<= | >>= | & | ^ | | | &= | ^= | |= | ~ | |
[] | () | , | ->* | -> | new | new[] | delete | delete[] |