Ogni linguaggio di programmazione e` concepito per soddisfare
determinati requisiti; i linguaggi procedurali (come il C) sono stati concepiti
per realizzare applicazioni che non richiedano nel tempo piu` di poche modifiche.
Al contrario i linguaggi a oggetti hanno come obiettivo l'estendibilita`, il
programmatore e` in grado di estendere il linguaggio per adattarlo al problema
da risolvere, in tal modo diviene piu` semplice modificare programmi creati
precedentemente perche` via via che il problema cambia, il linguaggio si
adatta. Famoso in tal senso e` stato FORTH, un linguaggio totalmente
estensibile (senza alcuna limitazione), tuttavia nel caso di FORTH questa
grande liberta` si rivelo` controproducente perche` spesso solo gli ideatori
di un programma erano in grado di comprendene il codice.
Anche il C++ puo` essere esteso, solo che per evitare i problemi di FORTH vengono
posti dei limiti: l'estensione del linguaggio avviene introducendo nuove classi,
definendo nuove funzioni e (vedremo ora) eseguendo l'overloading degli operatori;
queste modifiche devono tuttavia sottostare a precise regole, ovvero essere
sintatticamente corrette per il vecchio linguaggio (in pratica devono seguire
le regole precedentemente viste e quelle che vedremo adesso).
Le prime regole
Cosi` come la definizione di classe deve soddisfare
precise regole sintattiche e semantiche, cosi` l'overloading di un operatore deve
soddisfare un opportuno insieme di requisiti:
< ReturnType > operator@( < ArgumentList > ) { < Body > }ReturnType e` il tipo restituito (non ci sono restrizioni); @ indica un qualsiasi simbolo di operatore valido; ArgumentList e` la lista di parametri (tipo e nome) che l'operatore riceve, i parametri sono due per un operatore binario (il primo e` quello che compare a sinistra dell'operatore quando esso viene applicato) mentre e` uno solo per un operatore unario. Infine Body e` la sequenza di istruzioni che costituiscono il corpo dell'operatore.
struct Complex { float Re; float Im; }; Complex operator+(const Complex & A, const Complex & B) { Complex Result; Result.Re = A.Re + B.Re; Result.Im = A.Im + B.Im; return Result; }Si tratta sicuramente di un caso molto semplice, che fa capire che in fondo un operatore altro non e` che una funzione. Il funzionamento del codice e` chiaro e non mi dilunghero` oltre; si noti solo che i parametri sono passati per riferimento, non e` obligatorio, ma solitamente e` bene passare i parametri in questo modo (eventualmente utilizzando const come nell'esempio).
Complex A, B; /* ... */ Complex C = A+B;L'esempio richiede che sia definito su Complex il costruttore di copia, ma come gia` sapete il compilatore e` in grado di fornirne uno di default. Detto questo il precedente esempio viene tradotto (dal compilatore) in
Complex C(operator+(A, B));Volendo potete utilizzare gli operatori come funzioni, esattamente come li traduce il compilatore (cioe` scrivendo Complex C = operator+(A, B) o Complex C(operator+(A, B))), ma non e` una buona pratica in quanto annulla il vantaggio ottenuto ridefinendo l'operatore.
class Complex { public: Complex(float re, float im); Complex operator-() const; // - unario Complex operator+(const Complex & B) const; const Complex & operator=(const Complex & B); private: float Re; float Im; }; Complex::Complex(float re, float im = 0.0) { Re = re; Im = im; } Complex Complex::operator-() const { return Complex(-Re, -Im); } Complex Complex::operator+(const Complex & B) const { return Complex(Re+B.Re, Im+B.Im); } const Complex & Complex::operator=(const Complex & B) { Re = B.Re; Im = B.Im; return *this; }La classe Complex ridefinisce tre operatori. Il primo e` il - (meno) unario, il compilatore capisce che si tratta del meno unario dalla lista di argomenti vuota, il meno binario invece, come funzione membro, deve avere un parametro. Successivamente viene ridefinito l'operatore + (somma), si noti la differenza rispetto alla versione globale. Infine viene ridefinito l'operatore di assegnamento che come detto sopra deve essere una funzione membro non statica; si noti che a differenza dei primi due questo operatore ritorna un riferimento, in tal modo possiamo concatenare piu` assegnamenti evitando la creazione di inutili temporanei, l'uso di const assicura che il risultato non venga utilizzato per modificare l'oggetto. Infine, altra osservazione, l'ultimo operatore non e` dichiarato const in quanto modifica l'oggetto su cui e` applicato (quello cui si assegna), se la semantica che volete attribuirgli consente di dichiararlo const fatelo, ma nel caso dell'operatore di assegnamento (e in generale di tutti) e` consigliabile mantenere la coerenza semantica (cioe` ridefinirlo sempre come operatore di assegnamento, e non ad esempio come operatore di uguaglianza).
B = -A; // analogo a B.operator=(A.operator-()); C = A+B; // analogo a C.operator=(A.operator+(B)); C = A+(-B); // analogo a C.operator=(A.operator+(B.operator-())) C = A-B; // errore! // complex & Complex::operator-(Complex &) non definito.L'ultimo esempio e` errato poiche` quello che si vuole utilizzare e` il meno binario, e tale operatore non e` stato definito.
A = B = C = < Valore >che e` equivalente a
A = (B = (C = < Valore >));Non lo si confonda con il costruttore di copia: il costruttore e` utilizzato per costruire un nuovo oggetto inizializzandolo con il valore di un altro, l'assegnamento viene utilizzato su oggetti gia` costruiti.
Complex C = B; // Costruttore di copia /* ... */ C = D; // AssegnamentoUn'altra particolarita` di questo operatore lo rende simile al costruttore (oltre al fatto che deve essere una funzione membro): se in una classe non ne viene definito uno nella forma X::operator=(X&), il compilatore ne fornisce uno che esegue la copia bit a bit. Il draft
X & X::operator[](T Arg2);dove T puo` anche un riferimento o un puntatore.
class TArray { public: TArray(unsigned int Size); ~TArray(); int operator[](unsigned int Index); private: int * Array; unsigned int ArraySize; }; TArray::TArray(unsigned int Size) { ArraySize = Size; Array = new int[Size]; } TArray::~TArray() { delete[] Array; } int TArray::operator[](unsigned int Index) { if (Index<Size) return Array[Index]; else /* Errore */ }Si tratta di una classe che incapsula il concetto di array per effettuare dei controlli sull'indice, evitando cosi` accessi fuori limite. La gestione della situazione di errore e` stata appositamente ommessa, vedremo meglio come gestire queste situazioni quando parleremo di eccezioni.