C++ - enumerazioni e overloading degli operatori
In questa pagina 3
Altri due strumenti per costruire tipi: le enumerazioni, per scegliere fra pochi valori con un nome, e l'overloading degli operatori, per far funzionare +, ==, << sui tipi nuovi. Completano la classe Date di C++ - classi, membri e invariantiuna classe è un tipo definito dal programmatore che rappresenta un concetto con una rappresentazione (dati membro) e delle operazioni (funzioni membro); l'interfaccia pubblica separa chi usa la classe da chi la implementa e i dati privati sono modificabili solo tramite l'interfaccia; struct è una class con membri pubblici di default; l'invariante è la regola che definisce gli stati validi, e il costruttore deve stabilirlo e ogni funzione membro conservarlo; il costruttore (stesso nome della classe) inizializza con una lista di inizializzazione e, se i dati non sono validi, lancia un'eccezione, l'unico modo per far fallire un costruttore; le funzioni membro si definiscono fuori dalla classe (solo le brevissime dentro, dove sono inline); le funzioni che non modificano l'oggetto sono dichiarate const; le funzioni che si possono scrivere con la sola interfaccia pubblica (operator==, next_sunday) sono helper function esterne, spesso nello stesso namespace.C++ - classi, membri e invarianti → (il mese diventa un Month); un esempio completo di overloading, con le scelte di progetto, è il laboratorio Esercizio - Numeri razionali con classe e overloading (laboratorio 2). In Python l'analogo sono i metodi speciali (__add__, __eq__): Classi ed ereditarietà in PythonAttributi di istanza e di classe, metodi di istanza, di classe e statici; confronto e ordinamento con eq e lt; proprietà; ereditarietà, super(), override e polimorfismo; classi astratte come interfacce; eccezioni personalizzate; overloading e shadowing in Python.Classi ed ereditarietà in Python →.
Enumerazioni
In molte situazioni si vuole esprimere una scelta fra un numero predefinito di opzioni (un mese, un giorno della settimana, un semaforo). Si potrebbe usare un codice numerico, ma un insieme di etichette è più descrittivo per il programmatore (Month::feb contro 2) e il compilatore verifica che non si usi un valore a caso.
Definizione. Un'enumerazione (
enum) è un tipo definito dall'utente molto semplice: specifica un insieme di valori rappresentati da costanti simboliche (gli enumeratori); una variabile di quel tipo è una scelta in questo insieme.
enum class Month {
jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};
enum class Day {
monday, tuesday, wednesday, thursday, friday, saturday, sunday
};- Si possono scegliere valori specifici (
jan = 1); altrimenti ogni enumeratore vale il precedente più uno, partendo da 0. QuindiMonth::febvale 2 eDay::mondayvale 0. enum classcrea uno scope: l'enumeratore si scriveMonth::jan, il nomejanda solo non esiste. Due enumerazioni possono avere lo stesso enumeratore senza conflitti.
#include <iostream>
using namespace std;
enum class Month { // enumerazione "con classe": crea uno scope
jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};
enum class Day { // senza valori espliciti: parte da 0
monday, tuesday, wednesday, thursday, friday, saturday, sunday
};
enum Colore { rosso, verde = 5, blu }; // enum "semplice": i nomi stanno nello scope esterno
int main()
{
Month m = Month::feb; // il nome va qualificato: Month::feb
// Month m2 = feb; // errore: feb non e' in scope
// m = 7; // errore: non si assegna un int a un Month
// int n = m; // errore: nemmeno un Month a un int
Month mm = Month(7); // conversione ESPLICITA da int: jul
int n = int(m); // conversione esplicita a int: 2
cout << n << ' ' << int(mm) << '\n';
Day d = Day::wednesday;
cout << int(d) << ' ' << int(Day::sunday) << '\n'; // 2 e 6
Colore c = blu; // ok senza prefisso
int k = c; // un enum semplice si converte a int da solo
cout << rosso << ' ' << verde << ' ' << blu << ' ' << k << '\n';
return 0;
}2 7
2 6
0 5 6 6Regole di assegnamento di un enum class:
| istruzione | esito |
|---|---|
Month m = Month::feb; |
ok |
Month m2 = feb; |
errore: feb non è in scope |
m = 7; |
errore: nessuna conversione implicita da int |
int n = m; |
errore: nessuna conversione implicita a int |
Month mm = Month(7); |
ok: conversione esplicita da int (non controlla l'intervallo!) |
int n = int(m); |
ok: conversione esplicita a int |
Si può anche scrivere un enum senza class: i nomi stanno nello scope esterno (Colore c = blu;) e si convertono a int da soli. È più comodo ma più soggetto a conflitti: nel codice nuovo si preferisce enum class.
Overloading degli operatori
Gli operatori sono elementi fondamentali delle espressioni (C++ - tipi, variabili, espressioni e dichiarazioniun tipo definisce contenuto, intervallo di valori e operazioni; un oggetto è una regione di memoria con un tipo, il valore è la configurazione di bit letta in base al tipo, una variabile è un oggetto con un nome; inizializzazione (dare il primo valore) e assegnamento (cambiare il valore) sono diversi; le conversioni verso un tipo più capiente sono sicure, quelle verso un tipo meno capiente (narrowing, double->int, int->char) non lo sono e il compilatore le segnala di rado, ma la notazione con graffe {} le vieta; un'espressione calcola un valore da operandi e operatori, un lvalue è ciò che può stare a sinistra di =, constexpr vuole un valore noto in compilazione mentre const solo un valore che non cambia; gli operatori con side effect (++, --, +=) non vanno mischiati con altri usi della stessa variabile nella stessa espressione perché l'ordine di valutazione non è definito; una dichiarazione introduce un nome e un tipo, una definizione fornisce anche l'entità (memoria per una variabile, corpo per una funzione).C++ - tipi, variabili, espressioni e dichiarazioni →). Per i tipi built-in sono già definiti. Per un UDT? Sommare due date non ha un significato ovvio, ma può essere sensato definirlo.
Definizione. L'overloading degli operatori (operator overloading) è la possibilità di definire il comportamento di quasi tutti gli operatori per operandi di tipo definito dall'utente (
classoenum), scrivendo una funzione di nome speciale:operator+,operator++,operator*,operator[], ...
Il C++ riconosce questo schema e traduce:
Date d {2010, Month::feb, 21};
++d; // equivale a operator++(d) se è una helper function
// oppure a d.operator++() se è una funzione membroRegole
- Solo operatori già esistenti, con lo stesso numero di operandi e la stessa sintassi. Non si inventano operatori nuovi.
- Deve esserci almeno un argomento UDT:
int operator+(int, int);è un errore, perché+fraintè predefinito. - Un potente strumento, ma: non definire operatori con significato contro-intuitivo, e definirli solo se hanno senso concettualmente per quel tipo.
- Gli operatori più utili:
=,==,!=,<,[],(). - Ogni operatore ha il proprio schema, diverso se è funzione membro o funzione esterna:
int operator+(int, int); // errore: + built-in
Vector operator+(const Vector&, const Vector&); // OK
Vector operator+=(const Vector&, int); // OK come schema (ma += di solito è un membro che modifica *this)Helper function o funzione membro
| helper function | funzione membro | |
|---|---|---|
| argomenti per un operatore binario | due (almeno uno UDT, l'altro anche built-in) | uno: il primo operando è l'oggetto stesso (this) |
| UDT a sinistra obbligatorio? | no: int * Vec e Vec * int si scrivono entrambi |
sì: T + int sì, int + T no |
| accesso ai dati privati | solo se amica o con l'interfaccia pubblica | diretto |
| flessibilità | maggiore | minore |
#include <iostream>
using namespace std;
struct Vec {
int x, y;
Vec(int xx = 0, int yy = 0) : x{xx}, y{yy} {}
// FUNZIONE MEMBRO: l'operando di sinistra e' l'oggetto stesso (this), un solo argomento esplicito
Vec operator+(const Vec& b) const { return Vec{x + b.x, y + b.y}; }
Vec operator*(int k) const { return Vec{x * k, y * k}; } // Vec * int
};
// HELPER FUNCTION: due argomenti espliciti, il primo puo' essere di qualunque tipo
Vec operator*(int k, const Vec& v) { return v * k; } // int * Vec
bool operator==(const Vec& a, const Vec& b) { return a.x == b.x && a.y == b.y; }
ostream& operator<<(ostream& os, const Vec& v) { return os << '(' << v.x << ", " << v.y << ')'; }
int main()
{
Vec a {1, 2}, b {3, 4};
cout << a + b << '\n'; // a.operator+(b)
cout << a * 3 << '\n'; // a.operator*(3)
cout << 3 * a << '\n'; // operator*(3, a): possibile solo con l'helper function
cout << (a + b == Vec{4, 6}) << '\n';
// cout << 3 + a; // errore: nessun operator+(int, Vec) e 3 non puo' essere "this"
return 0;
}(4, 6)
(3, 6)
(3, 6)
1a + b e a * 3 sono chiamate a funzioni membro (a.operator+(b), a.operator*(3)). 3 * a richiede l'helper function operator*(int, const Vec&): con una funzione membro il 3 dovrebbe essere this, che non può essere un int. Per questo il codice 3 + a, se esiste solo il membro Vec::operator+, non compila:
error: invalid operands to binary expression ('int' and 'Vec')Per i binari che non modificano l'operando conviene la funzione esterna (così le conversioni implicite valgono per entrambi gli argomenti e a < b compila se compila b < a).
Pattern degli operatori più comuni
L'operatore << mostra perché deve restituire lo stream: cout << "Il valore " << i << " e' stato scritto" è una catena di chiamate: (cout << "Il valore ") << i << .... Ogni << prende lo stream a sinistra e restituisce lo stesso stream, così la chiamata dopo ha di nuovo uno stream a sinistra. Si traduce in operator<<(ostream&, string), poi operator<<(ostream&, int), ecc.
Pre e post incremento
operator++ ha due implementazioni:
T& operator++(T& t); // preincremento: ++x
T operator++(T& t, int); // postincremento: x++ (int: argomento fittizio, solo per distinguere)Il post-incremento restituisce una copia del valore precedente (non è un lvalue): ritorna T, di solito per valore. Il pre-incremento può ritornare T o T&, con comportamenti diversi: con T& il risultato è l'oggetto stesso, quindi si può concatenare ++(++c) (incrementa due volte); con T per valore il risultato è un temporaneo e la seconda ++ non compila (cannot increment value of type).
#include <iostream>
using namespace std;
struct Contatore { int v = 0; };
Contatore& operator++(Contatore& c) // restituisce un riferimento all'oggetto stesso
{
++c.v;
return c;
}
int main()
{
Contatore c;
++(++c); // il risultato di ++c e' c stesso: si incrementa due volte
cout << c.v << '\n';
return 0;
}2Il mese: operator++ e operator<<
Per un enum class conviene definire gli operatori che servono. L'incremento deve ricominciare da gennaio dopo dicembre; il ternario cond ? se_vero : se_falso lo scrive in una riga (int max = a > b ? a : b;, utile se le espressioni sono corte). La stampa usa una tabella dei nomi.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
enum class Month { jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };
// pre-incremento: passa al mese successivo (dopo dec torna a jan)
Month operator++(Month& m)
{
m = (m == Month::dec) ? Month::jan : Month(int(m) + 1); // operatore ternario
return m;
}
// post-incremento: l'int e' un argomento fittizio che distingue le due forme
Month operator++(Month& m, int)
{
Month vecchio = m; // il post-incremento restituisce il valore PRIMA dell'incremento
++m;
return vecchio;
}
const vector<string> month_tbl {"", "jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec"};
// l'operatore << deve restituire lo stream ricevuto, cosi' si puo' concatenare
ostream& operator<<(ostream& os, const Month& m)
{
return os << month_tbl[int(m)];
}
int main()
{
Month m = Month::nov;
cout << m << '\n'; // nov
++m;
cout << m << '\n'; // dec
++m;
cout << m << '\n'; // jan: ricomincia
Month a = m++; // a vale il vecchio valore (jan), m diventa feb
cout << a << ' ' << m << '\n';
cout << "Il mese e' " << m << '\n';
++m;
cout << "e il successivo " << m << '\n';
return 0;
}nov
dec
jan
jan feb
Il mese e' feb
e il successivo marOsservazioni: a = m++ assegna il valore prima dell'incremento (jan) e poi m diventa feb. L'operator<< delle slide è dichiarato con Month& m non costante; qui è const Month& m perché stampare non modifica, e così funziona anche con temporanei e oggetti const. Il vettore month_tbl ha una stringa vuota in posizione 0 perché i mesi partono da 1.
Domande d'esame
1. Che cos'è un'enumerazione e che cosa cambia con enum class? Può rendere il programma più efficiente?
Traccia. Tipo con un insieme di valori simbolici; enum class crea uno scope (Month::jan), niente conversioni implicite da e verso int. Rende il codice più espressivo e controllabile, non più efficiente. Non è una classe con membri pubblici: non ha funzioni né dati.
2. Quali vincoli ha l'overloading di un operatore? Posso cambiarne il numero di operandi? Il compilatore lo genera se non lo scrivo?
Traccia. Solo operatori esistenti, stessa arità e sintassi, almeno un argomento UDT; non si applica ai soli tipi built-in. Il numero di argomenti della funzione cambia (uno in meno se membro, perché il primo è this) ma non il numero di operandi. Se non lo definisco non c'è (tranne = per copia e spostamento che il compilatore genera).
3. Differenza fra Rational + int e int + Rational con operator membro e con helper function.
Traccia. Con il membro Rational::operator+(int) vale solo Rational + int (l'UDT è il primo operando, this); int + Rational richiede una helper function operator+(int, const Rational&).
4. Perché operator<< restituisce ostream&?
Traccia. Per concatenare più inserimenti nella stessa istruzione: ogni chiamata riceve lo stream e lo restituisce.
Versione ripasso
- Enumerazione = tipo con insieme di valori simbolici (enumeratori):
enum class Month { jan = 1, feb, ... };. Valori: quelli indicati, altrimenti il precedente (da 0).enum classcrea uno scope:Month::feb.
| istruzione | esito |
|---|---|
Month m = Month::feb; |
ok |
Month m2 = feb; |
errore: non in scope |
m = 7; / int n = m; |
errore: nessuna conversione implicita |
Month(7) / int(m) |
ok, conversione esplicita (nessun controllo dell'intervallo) |
Un enum senza class mette i nomi nello scope esterno e si converte a int. Rende il codice più espressivo, non più veloce.
- Overloading degli operatori = definire quasi tutti gli operatori per
class/enumcon una funzioneoperator+,operator++,operator[], ... Regole: solo operatori già esistenti, stesso numero di operandi e stessa sintassi, almeno un argomento UDT (int operator+(int,int)è un errore); non rendere gli operatori contro-intuitivi; i più utili sono=,==,!=,<,[],(). ++d;operator++(d)(helper) oppured.operator++()(membro).- Helper function: due argomenti per i binari, l'UDT può stare a sinistra o a destra (
Vec * inteint * Vec). Funzione membro: un argomento in meno (il primo operando èthis), l'UDT deve stare a sinistra:T + intsì,int + Tno meno flessibile.3 + acon solo il membro:invalid operands to binary expression ('int' and 'Vec').
struct Vec { int x, y;
Vec operator+(const Vec& b) const { return Vec{x+b.x, y+b.y}; } // membro
Vec operator*(int k) const { return Vec{x*k, y*k}; } };
Vec operator*(int k, const Vec& v) { return v * k; } // helper: 3 * a
// a{1,2}, b{3,4}: a+b = (4, 6); a*3 = (3, 6); 3*a = (3, 6)<</>>: helperostream& operator<<(ostream&, const T&)/istream& operator>>(istream&, T&); restituiscono lo stream per concatenare.- Pre e post incremento:
T& operator++(T&)(pre) eT operator++(T&, int)(post,intfittizio); il post restituisce la copia del vecchio valore (non è un lvalue). ConT&si può fare++(++c)(2 incrementi); conTper valore la seconda++non compila. - Ternario:
cond ? se_vero : se_falso; perMonth:m = (m == Month::dec) ? Month::jan : Month(int(m)+1);. Programma: danov:++dec,++jan;a = m++a = jan,m = feb. operator<<perMonth: tabellamonth_tblcon stringa vuota in posizione 0; parametroconst Month&(nonMonth&, che non accetta temporanei).- Traccia di risposta:
Rational + intva con membro o helper;int + Rationalsolo con helper;=sempre membro; le+ - * /restituiscono un nuovo oggetto per valore;+=restituisceT&. - Codice chiave del mese:
Month operator++(Month& m)
{ m = (m == Month::dec) ? Month::jan : Month(int(m) + 1); return m; }
ostream& operator<<(ostream& os, const Month& m) { return os << month_tbl[int(m)]; }- Domande tipiche:
enum class= scope + niente conversioni implicite, più espressivo, non più veloce; operatori = solo esistenti, arità uguale, almeno un UDT, se non li scrivi non esistono (eccetto copia e spostamento);Rational + intcon membro o helper,int + Rationalsolo con helper;<<restituisceostream&per concatenare. - Errori tipici:
operator<<che non restituisce lo stream;Month m = 7;; definireoperator+(int,int);operator<<come funzione membro; post-incremento che restituisce l'oggetto modificato; dimenticareconstsui parametri degli operatori. - Esercizi svolti: la classe
Rationalcompleta con tutti gli operatori e il confronto fra membro e helper: Esercizio - Numeri razionali con classe e overloading (laboratorio 2); la classeComplex: Esercizio - La classe Complex (primo appello 2022-23, programmazione). - Esempi di codice
enum:enum class Day { monday, ..., sunday };int(Day::wednesday) = 2,int(Day::sunday) = 6;enum Colore { rosso, verde = 5, blu };rosso = 0,verde = 5,blu = 6; unenumsemplice si converte aintda solo.
Esercizi su questo argomento
- Esercizio - Helper function e la classe Book (terzo appello 2021-22, teoria)
- Esercizio - La classe Complex (primo appello 2022-23, programmazione)
- Esercizio - Numeri complessi (quarto appello 2023-24, programmazione)
- Esercizio - Numeri razionali con classe e overloading (laboratorio 2)
- Esercizio - Quiz su constexpr, ordine di valutazione, dynamic_cast e lambda (primo appello 2022-23, quiz)
- Esercizio - Quiz su inizializzazione, free store, polimorfismo e passaggio di argomenti (quarto appello 2022-23, quiz)
- Esercizio - Quiz su operatore =, dangling pointer, enumerazioni e at() (quarto appello 2021-22, quiz)
- Esercizio - Quiz su record di attivazione, new[], dangling pointer e struct (terzo appello 2022-23, quiz)
- Esercizio - Rubrica di contatti con list e sort (primo appello 2023-24, programmazione)