L’overloading è uno degli aspetti fondamentali del C++. Infatti l’overloading non solo fornisce il sopporto per il polimorfismo in fase di compilazione ma aggiunge al linguaggio flessibilità e comodità.

Come ben sappiamo gli operatori del C ammettono solo gli operandi che appartengono ai tipi nativi del linguaggio; in C, ad esempio, non é ammesso "sommare" due oggetti, se questi sono istanze di una struttura. Grazie all’overloading è possibile forviare questo problema (estendibilità del C++).

Il C++ consente di effettuare l’overloading sia di funzioni, che degli operatori (salvo ::, ?:, sizeof, e pochi altri); consiste nell’impiegare lo stesso nome per due funzioni o dare un significato multiplo agli operatori. Il segreto dell’overloading sta nel fatto che ogni ridefinizione di una certa funzione (risp. operatore) deve utilizzare parametri di tipo differente, oppure in numero differente. Grazie a queste differenze, il compilatore sa quale funzione (risp. operatore) richiamare in una determinata situazione. In questo modo si riesce ad attribuire un nuovo significato agli operatori e alle funzioni.

Detto ciò passiamo ora a vedere come è possibile effettuare l’overloading delle funzioni e degli separatori. Per semplicità, nella maggior parte dei casi la possibile implementazione dei vari overloading è rimandata in futuro: vedi matrice, lista e albero.

 

Overloading delle funzioni

Per ottenere l’overloading di una funzione bisogna creare una funzione che il suo stesso nome, ma con differenti parametri. Chiariamo il concetto con un esempio:

#include <iostream>
using namespace std
int myfunc(int i);
double myfunc(double i); // è differente nel tipo dei parametri
int myfunc(int i, int j); // è differente nel numero dei parametri
int main()
{
cout<<myfunc(10)<<endl; // richiama myfunc(int i)
cout<<myfunc(6.5)<<endl; // richiama myfunc(int i)
cout<<myfunc(10, 20)<<endl; // richiama myfunc(int i, int j)
}
int myfunc(int i)
{
return i;
}
double myfunc(double i)
{
return i;
}
int myfunc(int i, int j)
{
return i+j;
}

Come si è detto la ridefinizione di una certa funzione deve utilizzare parametri di tipo differente e/o in numero differente, dunque due funzioni non possono differire solo per il tipo di dati restituiti. Ad esempio, ecco un modo errato di eseguire l’overloading della funzione myfunc():

int myfunc(int i);
char myfunc(int i);

Tra le funzioni modificate tramite overloading, quelle più importanti sono i costruttori. La forma più importante è costituita dal costruttore di copie. La creazione di un costruttore di copie può aiutare a evitare i problemi che sorgono quando si utilizza un oggetto per inizializzarne un altro. In tal caso si evita di utilizzare il costruttore bit a bit (i due oggetti saranno due copie esatte, ma non utilizzeranno la stessa area di memoria). Vi sono tre motivi che spingono ad eseguire l’overloading di una funzione costruttore: la maggiore flessibilità, la possibilità di creare oggetti inizializzati e non inizializzati e la possibilità di definire costruttori di copie. Chiariamo il concetto con un esempio:

class myclass{
int x;
public:
// overloading del costruttori in più modi: c’è maggiore flessibilità
myclass(int a);
myclass(int a, int b);
myclass() {x = 0}; // overloading senza inizializzatore
myclass(int n) {x = n}; // overloading con inizializzatore
myclass(const myclass &C); // costruttore di copie
}

 

Overloading degli operatori

Per ottenere l'overloading di un operatore bisogna creare una funzione con nome operator seguito dal simbolo dell'operatore (es.: operator+). Se l'operatore si applicherà a una classe, la funzione può essere definita come metodo (pubblico) della classe, oppure può essere esterna, nel qual caso va inserita come friend nella definizione della classe. La scelta è abbastanza libera: in genere conviene che la funzione sia un metodo quando può modificare i suoi operandi; in altri casi è obbligatorio che sia friend. Gli argomenti della funzione devono corrispondere agli operandi dell'operatore. Ne consegue che per gli operatori unari è necessario un solo argomento, per quelli binari ce ne vogliono due (e nello stesso ordine, cioè il primo argomento deve corrispondere al left-operand e il secondo argomento al right-operand). Si consiglia, anche se non è obbligatorio, che gli operatori mantengano comunque qualche "somiglianza" con il loro significato originario.

Tuttavia bisogna ricordare che, se la funzione è metodo di una classe, il C++ aggiunge come primo argomento, nella traduzione interna della chiamata, il puntatore nascosto this all'oggetto in cui la funzione é incapsulata; ne consegue che la funzione può essere un metodo solo se il primo operando è l'oggetto stesso (e in tal caso il numero di argomenti della funzione deve essere ridotto di un'unità); in caso contrario la funzione deve essere friend esterna.

Chiariamo il concetto con un esempio: definita la classe A con una sua istanza a, e definita una variabile k di tipo int, vogliamo creare due overloading dell'operatore "+" in modo che abbiano significato le operazioni a + k e k + a (possibilmente con lo stesso risultato, per mantenere valida la proprietà commutativa dell'operazione). In entrambi i casi le funzioni devono restituire un oggetto della classe A, ma, mentre nel primo caso il primo operando è anch'esso oggetto della classe A e quindi la funzione può essere metodo della classe stessa, nel secondo caso no e pertanto la funzione deve essere esterna:

// operazione a + k
A A::operator+(int k)
// la funzione è un metodo di A (il primo operando è nascosto)

// operazione k + a
A operator+(int k, A a)
// la funzione è esterna e va dichiarata come friend nella definizione della classe
A: friend A operator+(int, A);

Nel caso che gli argomenti della funzione non debbano essere modificati, è buona norma prudenziale usare la parola-chiave const (seguitando nell'esempio):

A operator+(const int k, const A a)

Inoltre, il C++ lavora più rapidamente se gli argomenti sono passati byreference:

A operator+(const int& k, const A& a)