Overloading degli operatori

16 ottobre 2017

Nelle lezioni precedenti sono stati introdotti una serie di operatori che si applicano ai tipi predefiniti del linguaggio. Questi costrutti adottano una particolare sintassi che richiama la notazione infissa, tipicamente usata nelle espressioni aritmetiche o logiche. Ciò il rende particolarmente efficaci nel massimizzare l’espressività del codice.

Tuttavia, a livello implementativo, gli operatori sono funzioni a tutti gli effetti, il cui nome assume la forma operator@, dove con @ intendiamo uno qualsiasi tra gli operatori che abbiamo discusso in precendenza (+, -, &, =, etc.). Inoltre, nella veste di funzione, essi sono anche caratterizzati dal tipo del valore restituito, dal numero degli argomenti e dal loro ordine.

Equiparando di fatto i costrutti di operatore e funzione, il linguaggio C++ offre la possibilità di “ridefinire” gli operatori, di modo che essi siano applicabili anche ai tipi definiti dall’utente, che può quindi sfruttare i benefici di una sintassi snella e legibile.

Il caso degli operatori di assegnamento di copia e di spostamento è un esempio di overloading che mette in risalto la versatilità di tale strumento.

Tuttavia, questo processo, detto overloading degli operatori (o sovraccarico degli operatori), implica la capacità da parte del compilatore di tradurre una o più istruzioni dalla notazione infissa alla sintassi di invocazione delle funzioni.

Il sovraccarico di operatori in C++ è corredato da una serie di tecnicismi a supporto di tale traduzione, ma al contempo necessita dell’associazione di pratiche di buona programmazione per mantenere fattori quali simmetria e commutatività degli operatori.

Il problema principale consiste nella definizione dell’ambito di visibilità degli operatori sovraccaricati. Alcuni operatori infatti possono essere sovraccaricati solo come membri di classe (assegnamento, assegnamento di copia e spostamento) perchè per la loro implementazione è richiesto l’uso del membro this, altri solo come funzioni esterne alla classe (ad esempio gli operatori << e >> di inserimento ed estrazione da flusso), altri ancora indifferentemente nell’uno o nell’altro modo.

Tuttavia, per quest’ultima categoria, la modalità scelta può avere delle ripercussioni sulla loro applicabilità che sono spesso fonte di confusione.

Overloading degli operatori come membri di classe

Sovraccaricare un operatore come membro di classe ha per effetto quello di tradurre la sua invocazione nel modo seguente:

a @ b  =>   a.operator@(b)  // operatore binario (+,-,% ...)
@a     =>   a.operator@()   // operatore unario  (!)

Nel caso di operatore binario, l’operatore @ viene invocato dall’istanza a, in quanto funzione membro di classe, ed ha come argomento b. Si noti quindi che a @ b e b @ a, hanno una semantica diversa non solo per le operazioni non commutative, ma in ragione del fatto che diversa è l’istanza che invoca la funzione membro.

Come esempio pratico, nel listato seguente consideriamo la definizione della classe Complex, dotata di operatori sovraccaricati per effettuare alcune operazioni artimetiche sui numeri complessi.

// complex.h
#ifndef COMPLEX_NUMBER
#define COMPLEX_NUMBER

class Complex 
{
public:
	Complex(float r, float i);
	
	float getReal() const;
	void setReal(float num);
	
	float getIm() const;
	void setIm(float num);
	
	Complex operator+(const Complex& op);
	Complex operator-(const Complex& op);

private:
	float re;
	float im;

};

#endif // COMPLEX_NUMBER

Le funzioni operator+() e operator-() sono membri di classe che sovraccaricano rispettivamente l’uso degli operatori ‘+’ e ‘-‘ per implementare le operazioni di somma e sottrazione tra istanze della classe Complex. La loro implementazione è riportata nel listato seguente.

// complex.cpp
Complex Complex::operator+(const Complex& op)
{
	return Complex(re + op.re, im + op.im);
}


Complex Complex::operator-(const Complex& op)
{
	return Complex(re - op.re, im - op.im);
}

Il frammento seguente mostra l’invocazione di tali funzioni mediante la sintassi infissa e l’equivalente invocazione di funzione.

Complex a(1, 2);
Complex b(3, 4);

Complex sumAB = a + b; // a.operator+(b)
Complex subAB = a - b; // a.operator-(b)

La firma dei due operatori, nella versione proposta in questo esempio, presenta alcune peculiarità degne di nota.

Ad esempio, l’uso di un argomento passato per riferimento esclude la necessità di dover implementare un costruttore di copia per la classe Complex e ottimizza le prestazioni della chiamata a funzione.

Tuttavia non è obbligatorio, pertanto se la classe in esame è dotata di un costruttore di copia (e preferibilmente anche di spostamento) definito dal programmatore o di default, è perfettamente lecito modificare la firma dell’operatore in:

Complex operator+(Complex op);

Anche il qualificatore const a decorazione dell’argomento op passato per riferimento è opzionale. Tuttavia ricordiamo che l’uso di const reference in C++ ha la capacità di catturare un valore temporaneo. Pertanto, in questa forma, è possibile concatenare più operazioni di somma e sottrazione nella stessa espressione come mostrato nel frammento seguente:

// Queste istruzioni sono compilate solo se gli operatori +/- possono
// accettare risultati intermedi catturati da const reference

Complex sumABC = a + b - c; // a.operator+(b.operator-(c)); 
Complex subABC = a - b + c; // a.operator-(b.operator+(c));

Supponiamo adesso di voler aggiungere, come membro di classe, un’altro sovraccarico dell’operatore ‘+’, di modo che sia possibile sommare un numero complesso ed uno reale.

// complex.h
Complex operator+(float num);

// complex.cpp
Complex Complex::operator+(float num)
{
	return Complex(re + num, im);
}

Poichè il sovraccarico di operatori segue le stesse regole del sovraccarico delle funzioni, è perfettamente lecito includere tale membro nella definizione della classe Complex, e lasciare che il compilatore selezioni il sovraccarico più appropriato.

Tuttavia, la definizione di questo sovraccarico come membro di classe produce come effetto collaterale una perdita di simmetria rispetto la posizione dei due operandi, come mostrato nel frammento seguente:

Complex a(1, 2);
Complex b = a + 5; // a.operator+(5) ok!
Complex c = 5 + a; // errore!! il compilatore non trova un sovraccarico adeguato!

Esistono due approcci per superare questo problema, ed entrambi si basano sulla definizione dei sovraccarichi come funzioni esterne alla classe. Il primo contempla la definizione di più sovraccarichi dello stesso operatore, l’altro, più sofisticato, sfrutta la capacità del compilatore di applicare conversioni agli argomenti delle funzioni per selezionare il sovraccarico più appropriato.

Overloading degli operatori come funzioni esterne alla classe

Sovraccaricare un operatore come funzione esterna alla classe ha per effetto quello di tradurre la sua invocazione nel modo seguente:

a @ b  =>   operator@(a, b)  // operatore binario (+,-,% ...)
@a     =>   operator@(a)     // operatore unario  (!)

Sussiste sempre quindi una differenza semantica tra a @ b e b @ a per le operazioni non commutative. Tuttavia, come primo approccio per risolvere il problema di simmetria rilevato in precedenza, potrebbe essere sufficiente spostare la definizione del sovraccarico all’esterno della classe Complex, e fornirne una versione alternativa che renda invariante la posizione degli operarandi:

// complex.h
#ifndef COMPLEX_NUMBER
#define COMPLEX_NUMBER

class Complex 
{
	...
};

Complex operator+(const Complex& right, float left);
Complex operator+(float right, const Complex& left);

#endif // COMPLEX_NUMBER

L’implementazione dei due sovraccarichi è identica, l’unica differenza consiste nell’ordine degli argomenti nella firma.

// complex.cpp
Complex operator+(const Complex& right, float left)
{
	return Complex(right.getReal() + num, right.getIm());
}

Complex operator+(float right, const Complex& left)
{
	return Complex(right.getReal() + num, right.getIm());
}

Questa soluzione, seppure perfettamente funzionante, ha il problema di dover duplicare i sovraccarichi per ogni operatore, rendendo il codice inutilemente prolisso.

Un approccio più elegante consiste nel rispristinare la simmetria facendo ricorso all’uso di un costruttore di conversione implicita che sia in grado di costruire un’istanza della classe Complex a partire da una variabile di tipo float, come mostrato nel listato seguente:

// complex.h
#ifndef COMPLEX_NUMBER
#define COMPLEX_NUMBER

class Complex 
{
	...
	Complex(float num);
	...
};

Complex operator+(const Complex& right, const Complex& left);

#endif // COMPLEX_NUMBER
// complex.cpp
Comple::Complex(float num)
{
	re = num;
	im = 0;
}

Complex operator+(const Complex& right, const Complex& left)
{
	return Complex(right.getReal() + left.getReal(), right.getIm() + left.getIm());
}

Dotando la classe Complex di un costruttore di conversione implicita, il compilatore selezionerà la funzione operator+() indifferentemete dall’ordine degli argomenti, siano essi di tipo float o Complex, poichè esiste una catena di conversioni per gli argomenti di tipo float che fa uso del costruttore apposito.

L’uso del qualificatore friend

Posto che, in base alle nostre esigenze, abbiamo deciso di implementare i nostri operatori sovraccaricati come funzioni esterne alla classe, è possibile applicare il qualificatore friend di modo accedere a tutti i membri della classe, indifferentemente dal loro modificatore di accesso. La versione friend dell’operatore ‘+’ è riportata nel listato seguente:

// complex.h
#ifndef COMPLEX_NUMBER
#define COMPLEX_NUMBER

class Complex 
{
	...
};

friend Complex operator+(const Complex& right, const Complex& left);

#endif // COMPLEX_NUMBER
// complex.cpp
Complex operator+(const Complex& right, const Complex& left)
{
	return Complex(right.re + left.re, right.im + left.im);
}

Overloading degli operatori di inserimento ed estrazione da flusso

Nella lezioni precedenti abbiamo introdotto gli operatori << e >> nel contesto delle operazioni bit a bit tra interi. In questa sede esaminiamo il caso particolare del sovraccarico di tali operatori al fine di implementare le comuni operazioni di inserimento ed estrazione da flusso con le entità definite nella libreria <iostream>.

La particolarità in questo caso consiste nel fatto che tali operatori non possono essere definiti come membri di classe, poichè, visto l’ordine degli operandi, sarebbe necessario modificare le classi di libreria. Pertanto, laddove richiesto, essi sono comunemente definiti come funzioni esterne nelle classe definite dall’utente. Un esempio per la classe Complex è riportato nel listato seguente:

// complex.h
#ifndef COMPLEX_NUMBER
#define COMPLEX_NUMBER

class Complex 
{
	...
};

std::ostream& operator<<(std::ostream& output, const Complex& c);
std::istream& operator>>(std::istream& input, Complex& c);

#endif // COMPLEX_NUMBER

Il fatto che il valore restituito sia una reference all’entità del flusso consente di concatenare più operazioni di inserimento o estrazione in sequenza.

// complex.cpp

std::ostream& operator<<(std::ostream& output, const Complex &c)
{
	std::cout << c.getReal() << std::showpos << c.getIm() << "i" << std::noshowpos << std::endl;
	
	return output;
};

std::istream& operator>>(std::istream& input, Complex& c)
{
	float val = 0;
	input >> val;
	c.setReal(val);

	val = 0;
	input >> val;
	c.setIm(val);
	
	return input;
}

Regole generali per il sovraccarico degli operatori

In generale, a parte i casi qui presi in considerazione, il sovraccarico degli operatori in C++ è associato ad alcune regole di buona programmazione e di buon senso.

Innanzi tutto la funzione dell’operatore sovraccaricato dovrebbe essere simile in linea teorica a quella dell’operatore nativo: ad esempio ‘+’ è usato per calcolare la somma di qualcosa, e non, ad esempio, per fare la moltiplicazione. Nobili eccezioni a questa regola sono gli operatori di inserimento ed estrazione dal flusso, ma la regola generale mantiene, nella consuetudine, la sua validità.

Alcuni operatori devono essere compatibili con quelli standard; ad esempio ->, deve avere per valore di ritorno un puntatore, o un oggetto per il quale sia a sua volta definito il sovraccarico di ->.

Inoltre, le regole di precedenza e di associazione rimangono invariate in caso di sovraccarico, e non possono essere modificate. Alcuni operatori come ‘::’ e ‘?’ non possono essere sovraccaricati affatto.

In virtù di questi molteplici fattori quindi, è sempre bene controllare scrupolosamente la documentazione ufficiale dello standard di riferimento al fine di prevenire errori in fase di compilazione e comportamenti inattesi a tempo di esecuzione.

Tutte le lezioni

1 ... 38 39 40 ... 65

Se vuoi aggiornamenti su Overloading degli operatori inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Overloading degli operatori

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy