Up Up Up Funzioni in C++

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:

axybc
00axybc0000
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

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:

2 7
3 6
(3, 6); 
File es1.log:

(3, 6)
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)

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


#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.


Up Up Up Funzioni in C++