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:
- Non e` possibile definire nuovi operatori, si puo` solamente eseguire l'overloading
di uno per cui esiste gia` un simbolo nel linguaggio. Possiamo ad esempio
definire un nuovo operatore
* , ma non possiamo definire
un operatore ** .
Questa regola ha lo scopo di prevenire possibili ambiguita`.
- Non e` possibile modificare la precedenza di un operatore e non e` possibile
modificarne l'arieta` o l'associativita`, un operatore unario rimarra`sempre
unario, uno binario dovra` applicarsi sempre a due operandi; analogamente
uno associativo a sinistra rimmarra sempre associativo a sinistra.
- Non e` concessa la possibilita` di eseguire l'overloading di alcuni operatori,
ad esempio l'operatore ternario
? : , l'operatore sizeof
e gli operatori di cast e in particolare l'operatore .*
e l'operatore punto (per la selezione dei campi di una struttura).
- E` possibile ridefinire un operatore sia come funzione globale che come
funzione membro, i seguenti operatori devono tuttavia essere sempre funzioni
membro non statiche: operatore di assegnamento (
=
), operatore di sottoscrizione ( [ ] )
e l'operatore -> .
A parte queste poche restrizioni non esistono molti altri limiti, possiamo ridefinire
anche l'operatore virgola ( , ) e persino l'operatore chiamata
di funzione ( () ); inoltre non c'e` alcuna restrizione
riguardo il contenuto del corpo di un operatore: un operatore altro non e` che
un tipo particolare di funzione e tutto cio` che puo` essere fatto in una funzione
puo` essere fatto anche in un operatore.
Un operatore e` indicato dalla keyword operator seguita dal
simbolo dell'operatore, per eseguirne l'overloading come funzione globale bisogna
utilizzare la seguente sintassi:
< 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.
Ecco un esempio di overloading di un operatore come funzione globale:
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).
Definito l'operatore, e` possibile utilizzarlo secondo l'usuale sintassi riservata
agli operatori, ovvero come nel seguente 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 A, B;
/* ... */
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.
Quando un operatore viene ridefinito come funzione membro il primo parametro
e` sempre l'istanza della classe su cui viene eseguito e non bisogna indicarlo
nella lista di argomenti, un operatore binario quindi come funzione globale
riceve due parametri ma come funzione membro ne riceve solo uno (il secondo
operando); analogamente un operatore unario come funzione globale prende un
solo argomento, ma come funzione membro ha la lista di argomenti vuota.
Riprendiamo il nostro esempio di prima ampliandolo con nuovi operatori:
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 B;
}
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).
Ecco alcuni esempi di applicazione dei precedenti operatori e la loro rispettiva
traduzione in chiamate di funzioni (A , B e C
sono variabili di tipo Complex ):
B = -A; // B.operator=(A.operator-());
C = A+B; // C.operator=(A.operator+(B));
C = A+(-B); // 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.
Passiamo ora ad esaminare con maggiore dettaglio alcuni operatori che solitamente
svolgono ruoli piu` difficili da capire.
|