Funzioni in C++
Funzioni inline
Se una funzione contiene poche linee di codice, senza loop, puo' essere
dichiarata inline. Questo significa che il suo codice sara' inserito
ovunque la funzione e' usata, quando il codice e' compilato
con ottimizzazione (e.g.
-O2; altrimenti inline non ha alcun effetto).
L'effetto e' simile a quello di una funzione macro,
ma con controllo dei tipi. In entrambi i casi l'esecuzione e' piu' veloce
che con una funzione non inline,
in quanto non c'e chiamata di funzione a run-time. Le funzioni inline
sono piu' sicure delle funzioni macro.
Ecco una macro non definita bene:
#include <iostream>
#include <cassert>
#include <cmath>
#include <cfloat>
#define HYPOTHENUSE(a,b) sqrt (a * a + b * b)
using namespace std;
int main ()
{
cout << "Illustrating simple gotcha for macros" << endl;
double x = 3.0, y = 4.0;
double res = HYPOTHENUSE(x,y); assert(fabs(res - 5.0) < DBL_EPSILON);
res = HYPOTHENUSE(y-1.0,y); assert(fabs(res - 3.8) < 0.1);
cout << "OK....." << endl;
}
|
e la corrispondente funzione inline
#include <iostream>
#include <cassert>
#include <cmath>
#include <cfloat>
using namespace std;
inline double hypothenuse (double a, double b){ return sqrt (a * a + b * b); }
int main ()
{
cout << "Illustrating inline functions" << endl;
double x = 3.0, y = 4.0;
double res = hypothenuse(x,y); assert(fabs(res - 5.0) < DBL_EPSILON);
res = hypothenuse(y-1.0,y); assert(fabs(res - 5.0) < DBL_EPSILON);
cout << "OK" << endl;
}
|
Esercizio (macro) Aggiungere parentesi nella
funzione macro in modo che HYPOTHENUSE(y-1.0,y) e
hypothenuse(y-1.0,y) diano lo stesso risultato.
Tuttavia HYPOTHENUSE(++x, ++y) e hypothenuse(++x, ++y)
differiranno ancora.
Notare che inline e' accettato come un suggerimento
da parte del compilatore,
che poi decide se adottarlo o no in base a considerazioni di efficienza.
Le funzioni inline non possono essere funzioni di libreria, in quanto
e' il compilatore e non il linker ad inserire la funzione nel codice
che la utilizza.
Esercizio (inline) Il programma nm
da' una lista di simboli dei file oggetto. Verificare con questo
che compilando l'esempio qui sopra con ottimizzazione hypothenuse
non compare nella lista dei simboli, mentre compare senza ottimizzazione.
I metodi definiti all'interno del body di una classe sono automaticamente
inline.
The inline function must still appear in the same file as the class interface, and cannot be compiled to be stored in, e.g., a library. The reason for this is that the compiler rather than the linker must be able to insert the code of the function in a source text offered for compilation. Code stored in a library is inaccessible to the compiler. Consequently, inline functions are always defined together with the class interface.
Parametri di default
E' possibile definire parametri di default per le funzioni; devono
essere gli ultimi parametri della funzione.
#include<cassert>
int test (int a, int b = 7, int c = 7) { return a + b + c; }
int main ()
{ assert(test(14,5,1) == 20 && test(14,5) == 26 && test(14) == 28); }
|
Se viene dato il prototipo di una funzione, i valori di default vanno
nella dichiarazione, non nella definizione della funzione
#include<cassert>
int test (int, int = 7, int = 7);
int main ()
{ assert(test(14,5,1) == 20 && test(14,5) == 26 && test(14) == 28); }
int test (int a, int b, int c) { return a + b + c; }
|
Un caso particolarmente significativo e' il costruttore di default;
se un costruttore ha tutti i parametri di default, allora e'
anche un costruttore di default.
Non ci puo' essere piu' di un costruttore di default.
Nel seguente esempio
Point c; chiama il costruttore di default.
#include <cassert>
struct Point
{
int x;
int y;
Point(int xx=0, int yy=0): x(xx), y(yy){}
};
int main(){
Point a(1,2);
Point b(2);
Point c;
Point d = 5;
assert(a.x == 1 && b.x == 2 && c.x == 0 && d.x == 5);
}
|
Point d = 5; conversione implicita da int a Point
dovuta al fatto che il costruttore puo' essere chiamato con un solo
parametro. Come detto in Constructor, per
evitare questa conversione implicita basta aggiungere la keyword
explicit di fronte al costruttore.
Esempi dalla libreria standard string:
#include <iostream>
#include <string>
#include<cassert>
using std::string; using std::cout; using std::endl;
int main(){
string s1("default salt");
string s2(s1,4,2);
cout << s2 << "\tnpos = " << string::npos << endl;
string s3(s1,4);
cout << s3 << endl;
assert(s1.find("lt") == 5 and s1.find("lt",6) == 10);
assert(s1.find("zx") == string::npos and string::npos == string::size_type (-1));
}
|
Output:
ul npos = 4294967295
ult salt
|
string(const string& s, size_type pos, size_type n = npos)
costruttore di copia di n caratteri della stringa
s a partire dalla posizione pos.
size_type find(const char* s, size_type pos = 0) const
posizione di una C-stringa entro la stringa; se non c'e', ritorna
il piu' grande valore possibile del tipo string::size_type
Decorazione dei nomi
Overloading di funzioni
Differenti funzioni possono avere lo stesso nome purche' qualcosa permetta
di distinguerle: numero di parametri, tipo di parametri (non tipo di ritorno).
#include <cassert>
typedef int my_int;
double test (double a, double b) { return a + b; }
int test (int a, int b) { return a - b; }
// int test (my_int a, my_int b) { return a * b; }
int main ()
{
double m = 7, n = 4;
int k = 5, p = 3;
assert(test(m, n) > 10.9 && test(k, p) == 2);
my_int a = 3, b = 4;
assert(test(a, b) == -1);
}
|
Il compilatore assegna nomi diversi a queste funzioni; diamo un'occhiata
al codice assembler es.s prodotto con
g++ -S es.C .
Cerchiamo con l'editore di testo test; vediamo che si sono
due funzioni _Z4testii e _Z4testdd.
Il compilatore decora il nome della funzione con i nomi degli tipi degli
argomenti.
Esercizio (fun_overload)
generare la versione in assembler con
g++ -S es.C
e guardare come e' decorato il nome della funzione.
Esempio di overloading dalla libreria standard string:
#include <iostream>
using namespace std;
int main(){
string s1("abc");
char cs1[] = "xy";
s1.insert(1, cs1);
cout << s1 << endl;
string s2("000000");
s2.insert(2, s1);
cout << s2 << endl;
}
|
Output:
string& insert(size_type pos, const string& s) inserisce la
stringa s in posizione pos;
string& insert(size_type pos, const char* s)
inserisce la
C-stringa s in posizione pos
Extern "C"
Dato che C++ adopera la decorazione dei nomi e C non la adopera,
per chiamare una funzione C in un programma C++ occorre istruire
g++ di non decorare la funzione C, quando ne trova la
dichiarazione. In questa maniera il linking dei file oggetto puo'
essere effettuato dal compilatore.
f1.c (file sorgente in C)
int foo(int i,int j ,int k) { return i+j+k;}
|
test1.C
#include <iostream>
extern "C" int foo(int,int,int);
int main(){ std::cout << foo(1,2,3) << std::endl; }
|
Compilazione:
cc -c f1.c
g++ -c test1.C
g++ f1.o test1.o -o test1
Togliendo la direttiva extern "C" si avrebbe
U _Z3fooiii
in test1.s, mentre si avrebbe
foo in f1.s (ottenuto con gcc -S f1.c) per
cui il linker non potrebbe utilizzare la definizione di foo
per trasformare test1.o in test1.
Se ci sono piu' funzioni, si puo' adoperare un blocco
extern "C" {
int foo(int,int,int);
int bar(int,int);
}
Le librerie in C sono spesso predisposte per essere utilizzate
in C++, ed hanno gli header files con
#ifdef __cplusplus
extern "C" {
...
}
#endif
I compilatori C++ definiscono sempre la macro __cplusplus.
Funzioni operatore
Uno dei principi base del C++ e' di permettere la creazione di tipi
definiti dall'utente che siano il piu' possibile simili, nell'uso,
ai tipi base.
Ad esempio per gli array esiste l'operatore indice (e.g. ary[3] )
per accedere agli elementi degli array. Usando il metodo operatore indice
operator[] si puo' avere un comportamento analogo per un
tipo definito dall'utente.
Le funzioni operatore hanno il prototipo
Tipo_di_ritorno operator op(lista argomenti);
dove operator e' una keyword, e
op puo' essere uno dei seguenti operatori seguenti
+ - * /
+= -= *= /= ++ --
== != > >= < <=
<< >> [] ()
new new[] delete delete[]
int double char
(ed altri).
I corrispondenti operatori metodo hanno un argomento in meno.
Gli operatori = , () , [] e ->
possono essere solo operatori metodo.
L' overloading di operatori puo' essere usato per definire le operazioni
simboliche di base per nuovi tipi di oggetti; la sintassi per gli operatori
rimane la stessa (e.g. operatore binario, precedenza di operatori)
anche quando essi sono applicati ai nuovi oggetti.
Ad esempio l'operatore
di moltiplicazione * e' un operatore binario, definito
sui vari tipi numerici; estendiamone l'uso usando operator*;
per effettuare l'operazione intero * vettore adoperiamo
un metodo operatore della classe Vector
Vector Vector::operator* (int a)
per effettuare l'operazione vettore * intero adoperiamo
una funzione operatore binaria
Vector operator*(int a, const Vector& v)
(gli interi non sono oggetti di una classe, per cui
non si possono utilizzare metodi in questo caso).
#include <cassert>
struct Vector
{
int x, y;
Vector(int xx=0, int yy=0): x(xx),y(yy){ }
int operator [] (int i) { return (i%2 == 0) ? x: y; }
Vector operator * (int a) { return Vector(x*a,y*a); }
};
inline Vector operator*(int a, const Vector& v) { return Vector(a * v.x,a * v.y); }
int main ()
{
Vector k(1,2), m;
m = k * 3; // k.operator*(3)
assert(m.x==3 && m.y==6 && k[0] == 1);
m = k.operator*(5);
assert(m.x==5 && m.y== 10);
m = 4 * k; // operator*(4,k)
assert(m.x==4 && m.y==8);
}
|
operator[] si chiama subscription operator (operatore indice).
Esercizio (op_fun)
Aggiungere all' esempio precedente il
metodo operatore che fa il prodotto interno v1*v2 di due vettori.
Esempio dalla libreria standard string:
#include <cassert>
#include <string>
using std::string;
int main(){
string s1("abc");
assert(s1[1] == 'b');
s1[1] = 'B';
assert(s1[1] == 'B');
string s2("de");
s1.append(s2);
assert(s1 == "aBcde");
s1 += s2;
assert(s1 == "aBcdede");
}
|
const_reference operator[] (size_type) const ritorna l'n-esimo
carattere della stringa (solo lettura);
reference operator[](size_type) n-esimo
carattere della stringa (lettura e scrittura)
bool operator==(const string& s1, cost char* s2);
funzione che confronta una stringa ed una C-stringa
string& operator+=(const string& s); metodo che appende la
stringa s, equivalente ad append(s).
Operatore assegnamento per una classe
Dati due oggetti di una classe, l'espressione a = b assegna
b ad a (a deve essere gia' esistente);
questa operazione viene effettuata
dall'operatore assegnamento per la classe, che per default
assegna i valori membro a membro;
tuttavia tale assegnazione non e' accettabile in alcuni casi,
tra cui quello di un membro costante, come nell'esempio della struttura
Int, dove abbiamo visto che
se c'e' un membro costante (non static),
non e' possibile assegnare una struttura ad un'altra, perche' verrebbe
utilizzato l'operatore di assegnamento di default, che assegnerebbe una
costante in un'altra, il che non e' permesso. Per evitare questo
problema, definiamo un operatore di assegnamento che non effettua
l'assegnazione del membro costante.
Interfaccia int.h
#ifndef INT_H
#define INT_H
struct Int
{
int i;
const int Num;
static int count;
Int(int, int); // constructors
~Int();
bool is_positive();
Int(const Int &); // copy constructor
Int& operator=(const Int &); // assignment operator
Int add(const Int& a);
};
#endif
|
Implementazione int.C
#include <iostream>
#include "int.h"
int Int::count = 0;
Int::Int(int ii, int n): i(ii), Num(n)
{ count++; std::cout << "constructor:" << count << " objects" << std::endl;}
Int::~Int()
{ count--; std::cout << "destructor:" << count << " objects" << std::endl;}
Int::Int(const Int & a) : i(a.i), Num(a.Num)
{ count++;
std::cout << "copy constructor:" << count << " objects" << std::endl;
}
Int& Int::operator=(const Int & a)
{ i = a.i; std::cout << "assignment:" << count << " objects" << std::endl;
return *this;
}
Int Int::add(const Int& a) { return Int(i + a.i, Num); }
|
Test file:
#include <iostream>
#include <cassert>
#include "int.h"
using namespace std;
int main()
{
cout << "Illustating the copy constructor" << endl;
Int a(3,4);
assert(a.i == 3);
Int b(1,1);
cout << "assert(a.add(b).i == 4);" << endl;
assert(a.add(b).i == 4);
cout << "Int d = a; << endl" << endl;
Int d = a;
assert(d.i == 3);
d = b = a;
assert(d.i == b.i && b.i == a.i);
cout << "OK" << endl;
}
|
Output:
Illustating the copy constructor
constructor:1 objects
constructor:2 objects
assert(a.add(b).i == 4);
constructor:3 objects
destructor:2 objects
Int d = a; << endl
copy constructor:3 objects
assignment:3 objects
assignment:3 objects
OK
destructor:2 objects
destructor:1 objects
destructor:0 objects
|
Generalmente, quando occorre mettere il costruttore di copia
klass&(const klass &)
occorre mettere anche l'operatore di
assegnamento
klass& operatore=(const klass &) .
Costruttore, costruttore di copia, distruttore ed operatore
di assegnamento hanno un ruolo particolare nelle classi;
sono cosi' importanti che, se non sono definiti dal programmatore,
sono assegnati di default.
Nell'esempio precedente l'operatore di
assegnamento operator= ritornava come riferimento
l'oggetto *this
dopo averne modificato il dato membro i.
Cio' permette di concatenare
operazioni d = b = a; dato che l'espressione
b = a ha come valore di ritorno b.
Di solito si evita che l'operatore di
assegnamento operator= riassegni un oggetto a se stesso
(a = a).
A questo scopo si adopera lo statement
if(this == &b) return *this;
#ifndef INT1_H
#define INT1_H
struct Int
{ int i;
Int(int ii = 0): i(ii) { }
const Int operator+(const Int&) const;
Int& operator+=(const Int&);
bool operator==(const Int&) const;
Int& operator=(const Int&);
int operator!();
int operator()(int);
operator int () const;
};
struct Add
{ Int a;
Add(Int aa): a(aa){ }
void operator()(Int& b) const;
};
#endif
|
int1.C
#include "int1.h"
const Int Int::operator+(const Int& b) const { return Int(i + b.i); }
Int& Int::operator+=(const Int& b) { i += b.i; return *this; }
bool Int::operator==(const Int& b ) const { return i == b.i; }
Int& Int::operator=(const Int& b) { if(this == &b) return *this; i = b.i; return *this; }
int Int::operator!() {return -i;}
int Int::operator()(int j) { return i+j; }
Int::operator int () const { return i; }
void Add::operator()(Int& b) const { b += a;}
|
Test file
#include <cassert>
#include "int1.h"
int main()
{
int i;
Int a(2), b(3), c(5); assert(a + b == c); // operator+
a += b; assert(a == c); // operator+=
b = c; assert(a == b); // operator=
assert(!c == - 5); // operator!
assert(c(10) == 15); // Int::operator()
Add f(Int(7)); f(c); assert(c == Int(12)); // Add::operator()
i = (int) c; assert(i == 12); // operator int ()
}
|
In questo esempio ci sono
- l'operatore cast operator int ()
che permette il cast (int) c dove c e' un
oggetto della struttura Int.
- l'operatore invocazione di funzione
int Int::operator()(int)
c(10) e' equivalente a c.operator()(10) .
Oggetti funzione
Nell'esempio precedente c'e' anche l'operatore invocazione di funzione
Add::operator()(Int& b)
che permette di creare l'oggetto funzione
Add f(Int(7));
un oggetto funzione viene chiamato come una funzione f(c) .
Gli oggetti funzione hanno un ruolo simile ai puntatori a funzione,
ma sono piu' flessibili, come dimostra il prossimo esempio.
#include <iostream>
#include <algorithm>
void for_each(int* first, int* last, void (*f)(int)) { for ( ; first != last; ++first) f(*first); }
void print7(int i) { std::cout << i+7 << " ";}
void print8(int i) { std::cout << i+8 << " ";}
struct Add
{
int a;
Add(int aa): a(aa){ }
void operator()(int i){ std::cout << a + i << " ";}
};
struct Add_to
{
int a;
Add_to(int aa): a(aa){ }
void operator()(int& i){ i += a;}
};
using std::cout; using std::endl;
int main(){
int A[] = {1,3,2,4};
int N = sizeof(A)/sizeof(int);
for_each(A, A+N, print7); cout << endl;
for_each(A, A+N, print8); cout << endl;
std::cout << "Using std::for_each\n";
std::for_each(A, A+N, print8); cout << endl;
std::cout << "Using function objects\n";
Add f0(0), f7(7), f8(8);
std::for_each(A, A+N, f0); cout << endl;
std::for_each(A, A+N, f7); cout << endl;
std::for_each(A, A+N, f8); cout << endl;
std::cout << "Changing the array\n";
Add_to g7(7);
std::for_each(A, A+N, g7);
std::for_each(A, A+N, f0); cout << endl;
}
|
In questo esempio utilizziamo il semplice algoritmo for_each
che abbiamo visto in
array
ed adoperiamo anche l'algoritmo STL std::for_each,
che ha il vantaggio di funzionare con qualunque puntatore a funzione
oppure funzione oggetto.
Esercizio(obj_fun)
Un altro esempio di algoritmo della STL e' std::sort,
che e' una variante di quicksort
che ha garanzia di andamento N log(N) .
sort(first,last) ordina in senso crescente
sort(first,last,cmp) ordina secondo la funzione oggetto
cmp.
#include <iostream>
#include <algorithm>
void print(int i) { std::cout << i << " ";}
struct Compare
{
bool operator()(int x, int y){ return x < y;}
};
int main(){
int A[] = {19,14,3,18,4,10,9, 7, 5, 8, 17, 13, 15, 12, 2, 1, 11, 16, 6};
const int N = sizeof(A) / sizeof(int);
std::for_each(A, A + N, print);
std::cout << std::endl;
Compare cmp;
std::sort(A, A + N, cmp);
std::for_each(A, A + N, print);
std::cout << std::endl;
}
|
In questo esempio std::sort(A, A + N, cmp);
ha lo stesso effetto che std::sort(A, A + N); ,
perche la funzione oggetto fa il confronto che viene fatto di default
da sort(first,last) .
Dopo aver eseguito questo esempio, introdurre un membro intero int a;
in Compare in modo che faccia il confronto
x%a <= y%a
friend
Una funzione friend puo' accedere ai membri privati di una
classe.
#include <iostream>
using namespace std;
class X;
struct Y {
void f(X*);
};
class X
{
int i, j;
public:
X(int ii, int jj): i(ii), j(jj) {}
~X(){}
int read_i() const { return i;}
int read_j() const { return j;}
void write_i(int n) { if(n > 0) i = n; }
void write_j(int n) { if(n < 0) j = n;}
friend void Y::f(X*);
friend ostream& operator<<(ostream&, const X&);
};
void Y::f(X* x) {
x->i = x->i + 1;
x->j = x->j - 1;
}
ostream& operator<<(ostream& os, const X& x) {
os << "(" << x.i << ", " << x.j << ")";
return os;
}
int main(){
X x(2,7);
Y y;
cout << x.read_i() << " " << x.read_j() << endl;
y.f(&x);
cout << x.read_i() << " " << x.read_j() << endl;
cout << x << "; " << endl;
cerr << x << endl;
}
|
./es1 2> es1.log
Output:
File es1.log:
friend ostream& operator<<(ostream&, const X&);
e' la maniera standard di aggiungere ad una classe una funzione per
stampare in maniera personalizzata gli oggetti di tale classe.
Dato che ritorna un riferimento a ostream, e' possibile
concatenare gli output. Le funzioni operatore rispettano le regole
di precedenza dell'operatore corrispondente.
(cout << x) << "; "
dove ora viene adoperata la seguente funzione definita in ostream:
ostream& operator<<(ostream&, const char*)
Infine si adopera il manipolatore endl
(cout << x << "; ") << endl;
che e' una funzione
ostream& endl(ostream&);
che puo' essere adoperata come
endl(cout);
Di solito il puntatore alla funzione endl viene passato
come parametro alla funzione di ostream
ostream& operator<< (ostream& (*f)(ostream&);
In ostream sono definite diverse funzioni operatore
ostream& operator<< , ad esempio
ostream& operator<<(int)
che permettono a cout di "riconoscere" i tipi built_in.
Sia cout che cerr sono di tipo ostream;
nell'esempio precedente li abbiamo usati entrambi.
Analogamente, si puo' rendere friend una funzione globale,
od una intera classe.
Esercizio(friend)
- togliere la keyword friend e provare a compilare
- definire operator<< come funzione globale (non friend),
sostituendo
l'accesso diretto ai dati di x con l'utilizzo di metodi di
X.
Torniamo all'esempio con GMP in struct
riscritto nello stile di C++
#include <iostream>
#include <cstdlib>
#include <gmpxx.h>
using namespace std;
int main() {
mpz_class n = 1000000000;
n *= 200000;
cout << n << endl;
}
|
compilato con -lgmpxx.
Scrivete un programma che calcola il fattoriale nello stile di C++;
deve avere una forma molto simile a quella in
function
Usate il prototipo
mpz_class factorial(unsigned int n)
Template di funzioni
In diverse circostanze la definizione di una funzione non e' legata
ad un particolare tipo di parametri; consideriamo ad esempio una funzione
min(a,b), che restituisce il minimo fra i numeri a,b .
Una possibilita' sarebbe di usare l'overloading e dichiarare
int min(int,int);
double min(double,double);
continuando con float, char e magari con qualche tipo
derivato, con l'ordinamento stabilito con l'overloading di
operator<
Tuttavia il compilatore C++ e' capace di generare automaticamente
qualsiasi versione della funzione, adoperando un template di funzione.
#include <iostream>
#include <cassert>
template<class T>
const T& min (const T& a, const T& b)
{
return (a < b)? a: b;
}
float min(float a, float b)
{
std::cerr << "Using float min(float a, float b)\n";
return (a < b)? a:b;
}
int main ()
{
int i1 = 34, i2 = 6;
assert(min (i1, i2) == 6);
double d1 = 7.9, d2 = 32.1;
assert(min (d1, d2) < 8);
float d4 = 1.1, d5 = 1.2;
assert(min(d4, d5) < 1.15);
}
|
(Si puo' scrivere template<class T> oppure
template<typename T>).
Il compilatore genera le versioni richieste int min(int,int)
e double min(double,double), sostituendo il parametro di template
T rispettivamente con int e double.
Per il tipo float viene adoperata la versione con la
corrispondenza esatta di tipo.
Nella SLT ci sono
template <class T> const T& max(const T& a, const T& b);
template <class T, class BinaryPredicate>
const T& max(const T& a, const T& b, BinaryPredicate comp);
In questo esempio BinaryPredicate comp e' una funzione oggetto:
#include <cassert>
#include <algorithm>
struct Compare
{
int a;
Compare(int aa = 1000): a(aa) { }
bool operator()(int x, int y){ return x%a < y%a;}
};
int main(){
const int x = std::max(3, 9);
assert(x == 9);
Compare cmp10(10);
assert(std::max(131,2, cmp10) == 2);
}
|
Per facilitare il debugging mentre si definisce una funzione di
template, e' conveniente lavorare con un tipo specifico, magari usando
#define T1 int
Finito il debugging nel caso specifico, lo si trasforma in una funzione
di template, mettendo
template <typename T1>
Esercizio(template/accumulate)
Trasformare le funzioni in accumulate.h in template di funzioni, in
modo che il test in test_accum.C funzioni.
Notare che si e' sfruttato l'overloading per chiamare le due funzioni
con lo stesso nome.
Template di classe
I template di classe sono definiti analogamente ai template di funzione;
vi sono molti casi
in cui e' utile definire una classe per diversi tipi;
si puo' realizzare cio' adoperando tipi parametrici.
Header file class_templ.h
#ifndef CLASS_TEMPL_H
#define CLASS_TEMPL_H
#include <cmath>
template<typename ttype>
class Vector
{
public:
ttype x;
ttype y;
Vector (ttype = 0, ttype = 0);
double module ();
};
template<typename ttype>
Vector<ttype>::Vector (ttype a, ttype b): x(a), y(b) { }
template<typename ttype>
double Vector<ttype>::module () { return sqrt (x * x + y * y); }
#endif
|
Test file
#include <iostream>
#include <cassert>
#include "class_templ.h"
int main ()
{
Vector<int> s[100];
Vector<int> t[3] = { Vector<int> (4, 5), Vector<int> (5, 5), Vector<int> (2, 4) };
s[23] = t[2];
assert(s[23].x == t[2].x);
std::cout << t[0].module () << std::endl;
}
|
Nel main istanziamo il tipo parametrico T con
int.
Come nel caso dei template di funzione, alla keyword typename
si puo' sostituire la keyword class.
Se nel main() aggiungessimo variabili
di tipo Vector<double>, il compilatore creerebbe due
classi concrete, una classe Vector di tipo int ed
una di tipo double.
Il codice compilato sarebbe lo stesso che si otterrebbe da un codice
sorgente in cui le due classi fossero scritte esplicitamente.
La risoluzione dei tipi parametrici nei tipi concreti richiesti
dall'applicazione viene fatta a tempo di compilazione, per cui non
vi e' alcun costo in termini di tempo a run-time. Come vedremo,
questo ha portato allo sviluppo della libreria standard di template
(STL).
Esempi di template di classe nella STL
- La classe string e' una istanziazione di template
basic_string<char>
- La classe vector<T> e' un contenitore con accesso
casuale, simile all'array di C, ma la cui dimensione puo' variare.
#include <cassert>
#include <vector>
int main(){
std::vector<int> v1(2);
v1[0] = 1; v1[1] = 2;
assert(v1[0] == 1 and v1.size() == 2);
v1.push_back(4);
assert(v1[2] == 4 and v1.size() == 3);
}
|
vector(size_type n) costruttore di un vettore di n elementi;
reference operator[](size_type n) operatore indice che legge e
scrive l'n-esimo elemento;
const_reference operator[](size_type n) const operatore indice
che legge l'n-esimo elemento.
size_type size() const metodo che ritorna il numero di elementi
del vettore;
void push_back(const T&) metodo che inserisce un elemento alla fine
del vettore, incrementandone la size.
const_reference e' sinonimo di const T&;
size_type e' sinonimo di size_t;
questi sinonimi sono definiti con typedef entro la classe
vector<T> .
Esercizio (template/vector)
Fare un esempio con un vector<string>,
analogo all'esempio precedente.
One-Definition Rule (ODR)
Nell'esempio precedente le definizioni dei template di funzione membro
sono nello stesso file header in cui il template di struttura e' definito.
Includendo questo header in due file sorgenti diversi, i corrispondenti
file oggetto contengono entrambi le stesse definizioni prese da
quell'header.
La regola ODR evita che cio' dia problemi nel linking tra diversi
file che richiamano tale header file:
due definizioni identiche di una classe, un template o una funzione inline
sono accettabili in unita' di traduzione differenti. (Una unita' di traduzione
e' un file sorgente dopo l'inclusione degli header files via preprocessore).
E' meglio evitare di definire una variabile globale od una funzione
in un header file, per evitare questi problemi di linking.
Ecco un esempio in tre files:
es2.h
#ifndef ES2_H
#define ES2_H
const int NUMMAX = 100;
struct Point { double x, y;};
#endif
|
es2.C
#include <iostream>
#include "es2.h"
struct Int { int i;};
inline void bark(){ }
void bar(){ }
template <typename T>
T boar(T a) { return a; }
int main()
{ std::cout << NUMMAX << std::endl;
Point a = {1,2};
std::cout << "point a = {" << a.x << "," << a.y << "}" << std::endl;
}
|
es1.C
#include "es2.h"
struct Int { int i;}; // redefinition allowed
inline void bark(){ } // redefinition allowed
template <typename T> // redefinition allowed
T boar(T a) { return a; }
#ifdef ERR1
void bar(){ } // redefinition not allowed
#endif
int foo(){Point b = {7,8}; return NUMMAX + (int)b.y; }
|
Compilando con
g++ -D ERR1 es1.C es2.C
si ha un errore quando il compilatore fa il linking
perche' ci sono due definizioni di bar().
Lo standard prevede che con la keyword export si possano
mettere le definizioni dei template di funzione membro
in un file separato da quello in cui e'
definita la classe, ma cio' non e' implementato da g++
e dalla maggior parte degli altri compilatori.