Ridefinire gli operatori

Contenuti:
In C++, gli operatori (come quello di addizione o di moltiplicazione) sono considerati come delle vere e proprie funzioni. Essi sono già definiti per i tipi di parametri standard (i tipi "nativi"), e possiamo farne l'overloading, cioè possiamo sovraccaricarne il nome di significati, come facciamo per le normali funzioni.

Se ancora non conosci cos'è l'overloading di funzione fai un salto qui.

Riflettete che proprio questa è la conseguenza logica interessante del fatto che gli operatori non sono nient'altro che delle funzioni, che del resto è una banale particolarizzazione già nota a tutti i matematici.
Ripeto: se gli operatori sono funzioni, allora possiamo fare l'overloading degli operatori per dargli nuovi ulteriori definizioni che si adattino alla nostre proprie classi, proprio come facciamo l'overloading di funzioni.

Ma non dobbiamo dimenticarci che gli operatori hanno tutti un significato "naturale", e, anche se ovviamente possibile, non è una buona idea ad esempio definire l'operatore di addizione per moltiplicare vettori!

Funzioni friend oppure funzioni membro?

Si dichiara un operatore usando la keyword operator seguita dall' operatore stesso. Quando sovraccarichiamo un operatore, possiamo scegliere tra due strategie: possiamo dichiarare gli operatori come funzioni friend oppure come funzioni membro. Non c'è una soluzione migliore in assoluto, dipende dall'operatore.

dichiarare gli operatori come Funzioni Friend

Quando dichiari un operatore come una funzione friend, devi mettere nel prototipo della funzione i parametri che saranno utilizzati dall'operatore (quelli prima e dopo il segno + per esempio), e un valore di ritorno, cioè il risultato restituito dall'operatore.
class Complex
{
  float re, im;
public:
  Complex (float r = 0, float i = 0) { re = r; im = i; }
  friend Complex operator+ (Complex &, Complex &);
};

Complex operator+ (Complex &a, Complex &b)
{
  return Complex (a.re + b.re, a.im + b.im);
}

dichiarare gli operatori come Funzioni Membro

Per una funzione membro, il primo parametro è sempre la classe stessa (che è, come sappiamo, mai dichiarato e implicitamente trasmesso), e poi vanno indicati gli altri parametri.
class Complex
{
  float re, im;
public:
  Complex (float r = 0, float i = 0) { re = r; im = i; }
  Complex operator+ (Complex &);
};

Complex Complex::operator+ (Complex &a)
{
  return Complex (re + a.re, im + a.im);
}
In entrambi i casi, gli operatori possono poi essere utilizzati nella stessa maniera con cui si usano con i tipi standard:
{
  Complex a (1, 2);
  Complex b (3, 4);
  Complex c;
  ...
  c = a + b;           // Chiama la funzione di addizione
}

Lista degli operatori

Quando fate l'overloading di un operatore, dovete indicare lo stesso numero di argomenti dell'operatore "originale" se usate una funzione friend, e questo numero meno uno per una funzione membro. Segue una lista di operatori, presentati in ordine di priorità discendente (una versione per i browser che non supportano le tabelle è qui):

OperatoreNumero di parametri
(nel conto va incluso l'oggetto nel caso di funzioni membro)
Nota
()indeterminatoDevi usare una funzione membro.
[]2Devi usare una funzione membro.
->2Devi usare una funzione membro. Fanne l'Overload solo se ne hai davvero bisogno.
new delete1Per una classe, definiscili come funzioni membro (in tal caso sono automaticamente definite static). Possono anche essere definiti globalmente.
++ --1Pre-incremento (o pre-decremento).
++ --2 (2º inutilizzato)Post-in/decremento (il 2º argomento è di tipo int e non viene usato, serve solo per distinguere le forme postfisse e prefisse).
&1Pre-definito (ritorna "this"). Fai l'Overload solo se c'è davvero bisogno.
+ - ! ~ *1
(cast)1Vedi la descrizione seguente.
* / % + -2
<< >>2
< <= > == !=2
& ^ || && |2
=2Pre-definito (fa la copia dei membri). Devi usare una funzione membro.
+= -= *= /= %=2
&= ^= |= <<= >>=2
,2

Gli operatori () e []

L'operatore () è speciale perchè il numero dei suoi argomenti può essere variabile: possiamo definire molti operatori (), ciascuno con parametri diversi.
class Matrix
{
  int array[10][10];
public:
  // ritorna un elemento della matrice (per riferimento, in modo da essere in grado
  // di modificarlo; ad esempio posso scrivere Matrix matrix; ... matrix(2, 3) = 5;)
  int &operator()(int x, int y) { return array[x][y]; }

  // ritorna la somma degli elementi di una colonna
  // Ndt: si fa la convenzione che il primo indice sia quello di colonna e quindi il secondo è quello di riga
  int operator()(int x)
  { int s=0; for (int i=0; i<10; i++) s+=array[x][i]; return s; }
};

{
  Matrix matrix;
  ...
  int a = matrix (2, 2);  // elemento (2,2)
  int b = matrix (2);     // somma della seconda colonna
}
Quando usi l'operatore [], puoi ritornare un riferimento all'oggetto. Così sarai in grado non solo di leggere l'elemento, ma anche di scriverlo:
class Vector
{
  int array[10];
public:
  int & operator[](int x) { return array[x]; }
};
{
  Vector v;
  int a = v[2];   // ottiene il valore
  v[5] = a;       // imposta il valore
}

Conversioni con l'operatore =

L'operatore = deve essere definito per forza come una funzione membro. Quando i suoi argomenti sono di un tipo classe diverso o di un tipo predefinito, può essere usato per fare delle conversioni di oggetti da una classe all'altra o da un tipo nativo ad una classe, ma solo durante l'assegnamento.
class Complex
{
  float re, im;
public:
  Complex (float r = 0, float i = 0) { re = r; im = i; }
  Complex & operator= (float f) { re = f; im = 0; return *this; }
};
{
  Complex a (10, 12);
  a = 12;               // Chiamata dell'operatore=
}
Infatti l'operatore = come convertitore di tipo ha queste limitazioni: non può essere usato ovunque, ad esempio non può essere usato per convertire argomenti prima della chiamata di una funzione o per convertire da tipo classe a tipo nativo, proprio perchè non abbiamo accesso alle classi astratte rappresentate dai tipi nativi per poterlo definire lì dentro!

Un modo migliore per effettuare le conversioni di tipo da classe a classe e per poter fare anche quelle da classe a tipo nativo è usare gli operatori di cast descritti nel prossimo paragrafo.

Ndt: Sperimentate con questo esempio:

class test
{
public:
//  test(int) {}        // togli il commento iniziale per vedere che succede
  test & operator= (int)
  {
    // ...
    return *this;
  }
};

void foo(const test & c)        // oppure test c
{
}

int  main()
{
  foo(2);                       // qui una temporanea di tipo test viene creata
                                // e subito distrutta dopo il ritorno
  return 0;
}
Questo esempio mostra anche come i costruttori possono essere usati (anche implicitamente) come convertitori di tipo (da tipo nativo a classe o anche da classe a classe). Notare che nonostante venga creata una variabile temporanea e la si inizializzi, l'operatore=(int) non viene usato, in quanto non viene effettuato un assegnamento della variabile temporanea, perciò avrete un errore se il costruttore test(int) non c'è. Insisto su questo punto: se ad es. scrivete test t=3; la variabile t non viene creata e poi assegnata ma bensì creata e inizializzata e viene chiamato il costruttore test(int) e non prima il costruttore vuoto e poi l'operator=(int). test t=3; infatti è lo stesso di test t(3);. Notate quindi che in C++ c'è differenza tra inizializzazione e assegnamento. Sono due concetti un po' diversi.

Ndt: da notare che facciamo restituire all'operatore= un riferimento all'oggetto appena assegnato. Notare anche l'istruzione return *this; che deve esserci, a meno che non definite void il tipo di ritorno (questo però non permette di concatenare gli operatori di assegnamento come si fa in C con i tipi nativi; es.: a=b=c; a=b=8;).
Qual è la ragione per cui abbiamo preferito far restituire un riferimento, anzichè un valore come nel prototipo Complex operator= (float f); ? Giusto per evitare la chiamata del costruttore di copia di default per costruire un oggetto temporaneo da ritornare. Questo programma vi aiuterà a capire la differenza e a spiegare quello che avviene nel caso di assegnamenti multipli. Vi invito a sperimentare ulteriormente col vostro compilatore, per risolvere eventuali altri dubbi. Come potete vedere, un modo semplice per capire quali funzioni vengono chiamate è quello di far stampare delle stringhe.

#include <stdio.h>

class Complex
{
  float re, im;
public:
  Complex (float r = 0, float i = 0)
  {
    re = r;
    im = i;
  }
  Complex (const Complex& complex)
  {
     printf("costruttore di copia chiamato\n");
     // fa il lavoro di quello di default
     re = complex.re;
     im = complex.im;
  }
  Complex operator=(const Complex& complex)
  {
     printf("operator=(Complex&) chiamato\n");
     // fa il lavoro di quello di default
     re = complex.re;
     im = complex.im;
     return *this;   // oppure complex
  }
  Complex /*&*/ operator= (float f)     // provare a uncommentare il &
  {
    printf("operator=(float) chiamato\n");
    re = f;
    im = 0;
    return *this;
  }
};

int main()
{
  Complex a (100, 200);
  Complex b (10, 20);
  Complex c (1, 2);
  
  c = b = a = 12;

  return  0;
}

Operatori di cast

Gli operatori di cast consentono di convertire da classi in tipi standard. Per esempio, se dichiariamo un operator int() dentro una classe, saremo in grado di convertire un oggetto di questa classe in un valore intero. Questo operatore sarà chiamato anche implicitamente, dal compilatore.

Devono essere delle funzioni membro. Il tipo di ritorno non va mai indicato, perchè è implicitamente noto (ovviamente è lo stesso del nome dell'operatore).

class Complex
{
  float re, im;
public:
  Complex (float r = 0, float i = 0) { re = r; im = i; }
  operator float() { return re; }
};
void foo (float f)
{
  printf ("%f\n", f);
}
{
  Complex a (10, 12);
  float b = (float) a;  // Chiamata esplicita dell'operatore float()
  float c = a;          // Chiamata implicita dell'operatore float()
  foo ((float) a);      // Chiamata esplicita dell'operatore float()
  foo (a);              // Chiamata implicita dell'operatore float()
}
Si possono usare gli operatori di cast anche per convertire oggetti in altri oggetti. La sintassi è la stessa, basta usare il nome della classe a cui convertire invece di float nell'esempio precedente.

Gli operatori new e delete

Quando fai l'overload degli operatori new e delete, devi preoccuparti di realizzare l'allocazione di memoria. Questa è la ragione per cui non dovresti farlo, a meno che non sai esattamente quello che vuoi.

La loro dichiarazione deve essere:

#include <sys/types.h>
#include <new.h>

static void * operator new (size_t);
static void operator delete (void *, size_t);

C++