Menu

Overloading degli operatori in C++: +, == e << personalizzati

L'overloading degli operatori in C++ permette ai tuoi tipi di funzionare con operatori predefiniti come +, == e <<. Impara le regole membro vs non membro, come ridefinire gli operatori di confronto e di stream e le trappole su tipi di ritorno e operatore di assegnazione.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Far sembrare predefiniti i tuoi tipi

Sai già che std::string ti permette di scrivere a + b per concatenare e cout << s per stampare. Non sono trucchi speciali del compilatore: sono normali funzioni con nomi strani. L'overloading degli operatori è la funzionalità che permette alle tue classi di agganciarsi alla stessa sintassi, così un tipo Vector2 o Money si può sommare, confrontare e stampare esattamente come un int.

Il meccanismo è semplice, una volta visto: un'espressione come a + b è una scorciatoia. Il compilatore la riscrive come chiamata a una funzione chiamata operator+ e ne cerca una che corrisponda ai tipi degli operandi. Definisci quella funzione per la tua classe e a + b all'improvviso funziona. È in realtà una forma specializzata di overloading delle funzioni: valgono le stesse regole di risoluzione dei nomi, solo con nomi a forma di operatore.

Nota che la funzione riceve entrambi gli operandi per const&: l'aritmetica non dovrebbe modificare i suoi input, e i riferimenti evitano le copie. Restituisce un nuovo Vector2 per valore: p + q deve produrre un risultato nuovo senza toccare p o q, proprio come 2 + 3 non cambia 2.

Membro vs non membro

Ci sono due posti in cui definire un operatore: come membro della classe o come funzione libera (non membro). Come membro, l'operando sinistro è l'implicito this, quindi un operatore binario riceve un solo parametro esplicito:

Il const dopo la lista dei parametri conta: a + b non dovrebbe modificare a, quindi il membro è marcato const. Usa la forma membro per gli operatori legati per natura all'operando sinistro che non richiedono conversioni su di esso: +=, [], (), -> e gli operatori unari come -x o ++x.

Il limite dei membri: l'operando sinistro non può essere convertito. Con il membro operator+ qui sopra, a + 50 funziona (50 si converte in Money per il lato destro), ma 50 + a non compila: l'operando sinistro 50 è un int, e non puoi aggiungere una funzione membro a int. Un operatore non membro risolve il problema perché entrambi gli operandi sono parametri espliciti ed entrambi possono essere convertiti:

Regola pratica: rendi non membri gli operatori binari simmetrici (+, ==, *) così le conversioni funzionano su entrambi i lati; rendi membri gli operatori che devono modificare l'operando sinistro o sono legati a esso (+=, [], =).

Overloading dell'operatore di stream

L'operatore ridefinito più spesso in assoluto è << per la stampa. Non puoi renderlo membro della tua classe, perché l'operando sinistro è uno std::ostream (come cout), non il tuo tipo, e ostream non è tuo. Quindi è sempre un non membro che riceve lo stream per riferimento non const e lo restituisce:

Due dettagli fanno funzionare tutto. Lo stream viene passato e restituito per riferimento (ostream&): gli stream non si possono copiare, e restituire lo stesso stream è ciò che ti permette di concatenare cout << "p = " << p << "\n". Ogni << restituisce lo stream così il << successivo ha qualcosa a cui legarsi. Dimentica il return os; e la concatenazione si rompe.

Operatori di confronto

Per confrontare i tuoi oggetti con ==, < e simili, fai l'overloading degli operatori di confronto. Prima di C++20 li scrivevi tutti a mano; la trappola principale è che operator< deve restituire un bool e definire un ordinamento coerente:

Scrivere a mano tutti e sei i confronti (==, !=, <, <=, >, >=) è noioso e facile da sbagliare. C++20 ha aggiunto l'operatore di confronto a tre vie <=> (lo "spaceship"). Impostarlo a default insieme a == genera per te tutti i confronti:

= default dice al compilatore di confrontare i membri nell'ordine di dichiarazione, che è esattamente l'ordinamento lessicografico che scriveresti a mano. Preferiscilo con i compilatori moderni.

L'operatore di assegnazione e le sue trappole

operator= (assegnazione per copia) è speciale: il compilatore ne genera uno per te, e per le classi semplici quel default è corretto. Devi scriverne uno tuo solo quando la classe gestisce una risorsa, come la memoria grezza o un handle di file, dove una copia membro per membro sarebbe sbagliata. La firma canonica restituisce *this per riferimento così le assegnazioni si possono concatenare (a = b = c):

In questa breve funzione si nascondono due trappole. Prima, il controllo di autoassegnazione if (this == &other): senza di esso, a = a eseguirebbe delete[] data e poi leggerebbe dal other.data appena liberato, cioè undefined behavior. Seconda, l'ordine conta: in una versione scritta a mano non devi cancellare il vecchio buffer prima di aver copiato al sicuro quello nuovo (un'implementazione reale spesso alloca prima, o usa l'idioma copy-and-swap, così un'allocazione fallita lascia intatto l'oggetto).

Una trappola più generale: non ridefinire gli operatori in modi sorprendenti. Un operator+ che modifica di nascosto l'operando sinistro, o un operator== non simmetrico, confonderà chiunque legga e romperà il codice della libreria standard che presuppone i significati abituali. Fai l'overloading degli operatori solo quando l'operazione è davvero "simile all'addizione" o "simile all'uguaglianza" per il tuo tipo.

Prossimo passo: gli specificatori di accesso

Nota come ogni esempio abbia tenuto private i dati membro, esponendo il comportamento tramite una piccola superficie pubblica: costruttori, operatori e pochi metodi. Quel confine tra ciò che è visibile all'esterno e ciò che è nascosto dentro la classe è controllato dagli specificatori di accesso: public, private e protected. Nella prossima pagina vedremo esattamente cosa permette ciascuno, perché dati private con metodi pubblici sono il default di un buon incapsulamento e come protected si inserisce nell'ereditarietà.

Domande frequenti

Cos'è l'overloading degli operatori in C++?

L'overloading degli operatori ti permette di definire cosa significano operatori predefiniti come +, == o << per i tuoi tipi. Scrivi una funzione con un nome speciale, operator+, operator== e così via, e il compilatore la chiama ogni volta che l'operatore compare con operandi della tua classe. È così che string + string concatena e cout << obj stampa un oggetto personalizzato.

Gli operatori in C++ vanno definiti come funzioni membro o non membro (friend)?

Usa una funzione membro quando l'operando sinistro è la tua classe e non avrà bisogno di conversioni (per esempio +=, [], ()). Usa una funzione non membro (spesso friend) quando l'operando sinistro potrebbe essere un tipo predefinito o quando vuoi conversioni simmetriche su entrambi i lati: è obbligatorio per operator<<, perché l'operando sinistro è uno std::ostream, non la tua classe.

Quali operatori C++ non si possono ridefinire?

Non puoi fare l'overloading di :: (risoluzione dello scope), . (accesso ai membri), .* (accesso tramite puntatore a membro), ?: (ternario) e sizeof. Non puoi nemmeno inventare operatori nuovi o cambiare il numero di operandi o la precedenza di un operatore: + è sempre binario con la stessa precedenza, che sommi degli int o il tuo Vector2.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA