Un nome, tante versioni
Spesso ti serve la stessa operazione per tipi di dati diversi: stampare un int, stampare una string, stampare un double. In alcuni linguaggi inventeresti printInt, printString, printDouble. Il C++ ti permette di dare a tutte lo stesso nome e le distingue in base ai parametri. Questo è l'overloading delle funzioni.
La regola è semplice: più funzioni possono condividere un nome purché le loro liste dei parametri siano diverse, per numero di parametri, per tipo o per ordine. Il compilatore guarda gli argomenti in ogni punto di chiamata e sceglie per te la versione corrispondente.
Tre funzioni, un nome. Ogni chiamata finisce sulla versione il cui tipo di parametro si adatta all'argomento. È ciò che fa funzionare std::cout << x con int, double e stringhe allo stesso modo: operator<< è sovraccaricato molte volte.
Cosa rende diverso un overload
Un overload si distingue solo per la lista dei parametri. Puoi variare:
int area(int side); // 1 parametro
int area(int width, int height); // 2 parametri -> diversa
double area(double r); // tipo diverso -> diversa
void log(string msg, int level); // l'ordine conta...
void log(int level, string msg); // ...quindi anche questa è diversa
Ognuno di questi è un overload legale e distinto. Il compilatore costruisce un insieme di candidati con tutte le funzioni chiamate area, poi le abbina in base al numero e al tipo degli argomenti.
Il solo tipo di ritorno non basta
Ecco l'insidia in cui cadono quasi tutti: non puoi fare overloading sul tipo di ritorno. Il valore restituito non ha alcun ruolo nella scelta dell'overload, perché il compilatore decide quale funzione chiamare in base agli argomenti, prima ancora di guardare cosa viene restituito.
int convert(double x); // OK
double convert(double x); // ERRORE: ridefinizione, cambia solo il tipo di ritorno
Questo non compila. Se le liste dei parametri sono identiche, per l'overloading le due dichiarazioni sono la stessa funzione, e ottieni un errore di ridefinizione. Per distinguere in base al tipo del risultato, cambia un parametro (oppure usa i template o una conversione di tipo nel punto di chiamata).
Come la risoluzione dell'overload sceglie il vincitore
Quando fai una chiamata, il compilatore classifica ogni overload utilizzabile e sceglie la corrispondenza migliore. In linea di massima preferisce, in quest'ordine:
- Una corrispondenza esatta (nessuna conversione necessaria).
- Una promozione (per esempio
charoshort->int,float->double). - Una conversione standard (per esempio
int->double,double->int, puntatore alla classe base).
Se un solo overload è strettamente migliore di tutti gli altri, vince quello. Guarda come una corrispondenza esatta batte una conversione:
'A' è un char, ma una promozione a int si classifica meglio di una conversione a double, quindi viene chiamato l'overload int. Queste regole di classifica sono il motivo per cui la risoluzione dell'overload di solito "fa la cosa giusta", e ogni tanto ti sorprende.
La trappola dell'ambiguità
Se due overload sono ugualmente buoni, senza che nessuno sia strettamente migliore, il compilatore si rifiuta di tirare a indovinare e segnala una chiamata ambigua. Il caso da manuale è quello di due overload che richiedono ciascuno una conversione dello stesso rango:
void f(int x);
void f(double x);
f(0L); // ERRORE: ambigua, long -> int e long -> double sono conversioni dello stesso rango
Né int né double corrispondono esattamente a long, ed entrambe le conversioni hanno lo stesso rango, quindi la chiamata è ambigua. Hai due soluzioni pulite:
Una sorpresa correlata: passare un letterale stringa. void g(const string&) e void g(bool) si contenderanno entrambe g("hi"), e bool può vincere, perché un const char* si converte in bool (non nullo -> true) con meno passaggi rispetto alla costruzione di una std::string. Se vedi un letterale stringa che chiama misteriosamente il tuo overload bool, ecco perché: aggiungi un overload const char* o const string& per ottenere la corrispondenza esatta.
Overloading e argomenti predefiniti non vanno d'accordo
Gli argomenti predefiniti non sostituiscono l'overloading, e combinarli crea ambiguità. Entrambi possono rispondere alla stessa chiamata, quindi il compilatore non riesce a scegliere:
void connect(string host, int port = 8080); // si può chiamare con 1 argomento
void connect(string host); // si può chiamare anche questa con 1 argomento
connect("localhost"); // ERRORE: ambigua, entrambe corrispondono con un solo argomento
Scegli un solo approccio per ogni forma di chiamata. Usa gli argomenti predefiniti quando il comportamento è identico e vuoi solo parametri facoltativi; usa l'overloading quando le diverse liste di argomenti devono eseguire codice davvero diverso. Mescolarli in modo che due firme collidano per lo stesso numero di argomenti porta a un errore di ambiguità garantito.
Un'altra distinzione da fissare bene: l'overloading non è l'overriding. L'overloading si risolve in fase di compilazione tra funzioni dello stesso scope con lo stesso nome ma parametri diversi. L'overriding sostituisce una funzione virtual in una classe derivata a runtime e richiede la stessa firma: un argomento per le funzioni virtuali, più avanti.
Prossimo: Lambda
L'overloading dà a un nome più implementazioni tipizzate scelte in fase di compilazione. A volte però non vuoi affatto una funzione con un nome: ti serve una piccola funzione usa e getta definita proprio dove la usi, spesso da passare a un algoritmo come sort. È esattamente ciò che sono le lambda: funzioni anonime che puoi scrivere in linea, con cui catturare le variabili circostanti e da consegnare in un'unica espressione. Nella prossima pagina vedremo come scriverle e quando battono una funzione completa con un nome.
Domande frequenti
Cos'è l'overloading delle funzioni in C++?
L'overloading delle funzioni ti permette di definire più funzioni con lo stesso nome, purché le loro liste dei parametri siano diverse (per numero, tipo o ordine). Il compilatore sceglie quale chiamare in base agli argomenti che passi, quindi print(42) e print("hi") possono chiamare due funzioni print diverse.
Due funzioni C++ possono differire solo per il tipo di ritorno?
No. Gli overload devono differire nella lista dei parametri. int f(int) e double f(int) sono un errore di compilazione: il tipo di ritorno non fa parte della firma usata per la risoluzione dell'overload, perché il compilatore sceglie l'overload in base agli argomenti nel punto di chiamata, prima ancora che il valore restituito venga usato.
Cosa causa un errore di "chiamata ambigua" con le funzioni sovraccaricate?
Succede quando due overload corrispondono ugualmente bene e il compilatore non riesce a preferirne uno. Un caso classico è f(int) e f(double) chiamate con f(0L) (un long), dove entrambe richiedono una conversione dello stesso rango. Si risolve aggiungendo un overload a corrispondenza esatta o con una conversione di tipo dell'argomento al tipo che vuoi.