Puntatori a Funzione in C++
- I puntatori a funzione sono variabili che memorizzano l'indirizzo di una funzione.
- I puntatori a funzione possono essere utilizzati per chiamare funzioni in modo indiretto.
- I puntatori a funzione sono utili per implementare callback e funzioni di ordine superiore.
- La sintassi per dichiarare, assegnare e utilizzare i puntatori a funzione può essere complessa, ma può essere semplificata usando alias di tipo.
Puntatori a Funzioni
Un puntatore a funzione è proprio ciò che il nome suggerisce: un puntatore che denota una funzione piuttosto che un oggetto.
Come qualsiasi altro puntatore, un puntatore a funzione punta a un tipo particolare. Il tipo di una funzione è determinato dal suo tipo di ritorno e dai tipi dei suoi parametri. Il nome della funzione non è parte del suo tipo. Per esempio:
// confronta le lunghezze di due stringhe
bool confrontaLunghezza(const string &, const string &);
Qui, confrontaLunghezza ha tipo bool(const string&, const string&). Per dichiarare un puntatore che può puntare a questa funzione, dichiariamo un puntatore al posto del nome della funzione:
// pf punta a una funzione che restituisce bool
// e che prende due riferimenti const string
bool (*pf)(const string &, const string &); // *non inizializzato*
Partendo dal nome che stiamo dichiarando, vediamo che pf è preceduto da un asterisco *, quindi pf è un puntatore. Alla destra c'è una lista di parametri, il che significa che pf punta a una funzione. Guardando a sinistra, troviamo che il tipo che la funzione restituisce è bool. Quindi, pf punta a una funzione che ha due parametri const string& e restituisce bool.
Le parentesi attorno a *pf sono necessarie. Se omettiamo le parentesi, allora dichiariamo pf come una funzione che restituisce un puntatore a bool:
// ERRORE
// dichiara una funzione chiamata pf che restituisce un puntatore a bool
bool *pf(const string &, const string &);
Uso dei Puntatori a Funzioni
Quando usiamo il nome di una funzione come valore, la funzione viene automaticamente convertita in un puntatore. Per esempio, possiamo assegnare l'indirizzo di confrontaLunghezza a pf come segue:
// pf ora punta alla funzione chiamata confrontaLunghezza
pf = confrontaLunghezza;
// assegnazione equivalente: l'operatore address-of è opzionale
pf = &confrontaLunghezza;
Inoltre, possiamo usare un puntatore a una funzione per chiamare la funzione a cui il puntatore punta. Possiamo farlo direttamente e non è necessario dereferenziare il puntatore:
bool b1 = pf("ciao", "addio"); // chiama confrontaLunghezza
bool b2 = (*pf)("ciao", "addio"); // chiamata equivalente
bool b3 = confrontaLunghezza("ciao", "addio"); // chiamata equivalente
Non c'è conversione tra puntatori a un tipo di funzione e puntatori a un altro tipo di funzione. Tuttavia, come al solito, possiamo assegnare nullptr o un'espressione costante intera con valore zero a un puntatore a funzione per indicare che il puntatore non punta ad alcuna funzione:
string::size_type sommaLunghezza(const string&, const string&);
bool confrontaStringaC(const char*, const char*);
// OK: pf non punta ad alcuna funzione
pf = 0;
// ERRORE: il tipo di ritorno differisce
pf = sommaLunghezza;
// ERRORE: i tipi dei parametri differiscono
pf = confrontaStringaC;
// OK: i tipi di funzione e puntatore corrispondono esattamente
pf = confrontaLunghezza;
Puntatori a Funzioni Sovraccaricate
Come al solito, quando usiamo una funzione sovraccaricata, il contesto deve rendere chiaro quale versione viene utilizzata. Quando dichiariamo un puntatore a una funzione sovraccaricata
void ff(int*);
void ff(unsigned int);
// pf1 punta a ff(unsigned)
void (*pf1)(unsigned int) = ff;
il compilatore usa il tipo del puntatore per determinare quale funzione sovraccaricata utilizzare. Il tipo del puntatore deve corrispondere esattamente a una delle funzioni sovraccaricate:
// ERRORE: nessuna ff con una lista di parametri corrispondente
void (*pf2)(int) = ff;
// ERRORE: il tipo di ritorno di ff e pf3 non corrispondono
double (*pf3)(int*) = ff;
Parametri Puntatori a Funzione
Proprio come con gli array, non possiamo definire parametri di tipo funzione ma possiamo avere un parametro che è un puntatore a funzione. Come con gli array, possiamo scrivere un parametro che sembra un tipo funzione, ma sarà trattato come un puntatore:
// il terzo parametro è un tipo funzione e
// viene automaticamente trattato come puntatore a funzione
void usaPiuGrande(const string &s1,
const string &s2,
bool pf(const string &, const string &));
// dichiarazione equivalente:
// definisce esplicitamente il parametro come puntatore a funzione
void usaPiuGrande(const string &s1,
const string &s2,
bool (*pf)(const string &, const string &));
Quando passiamo una funzione come argomento, possiamo farlo direttamente. Sarà automaticamente convertita in un puntatore:
// Converte automaticamente la funzione
// confrontaLunghezza in un puntatore a funzione
usaPiuGrande(s1, s2, confrontaLunghezza);
Come abbiamo appena visto nella dichiarazione di usaPiuGrande, scrivere i tipi puntatore a funzione diventa rapidamente tedioso. Gli alias di tipo, insieme a decltype, ci permettono di semplificare il codice che usa puntatori a funzioni:
// Func e Func2 hanno tipo funzione
typedef bool Func(const string&, const string&);
// tipo equivalente
typedef decltype(confrontaLunghezza) Func2;
// FuncP e FuncP2 hanno tipo puntatore a funzione
typedef bool(*FuncP)(const string&, const string&);
// tipo equivalente
typedef decltype(confrontaLunghezza) *FuncP2;
Qui abbiamo usato typedef per definire i nostri tipi. Sia Func che Func2 sono tipi funzione, mentre FuncP e FuncP2 sono tipi puntatore. È importante notare che decltype restituisce il tipo funzione; la conversione automatica a puntatore non viene fatta. Poiché decltype restituisce un tipo funzione, se vogliamo un puntatore dobbiamo aggiungere l'asterisco * noi stessi. Possiamo ridichiarare usaPiuGrande usando qualsiasi di questi tipi:
// dichiarazioni equivalenti di usaPiuGrande usando alias di tipo
void usaPiuGrande(const string&, const string&, Func);
void usaPiuGrande(const string&, const string&, FuncP2);
Entrambe le dichiarazioni dichiarano la stessa funzione. Nel primo caso, il compilatore convertirà automaticamente il tipo funzione rappresentato da Func in un puntatore.
Restituire un Puntatore a Funzione
Come con gli array, non possiamo restituire un tipo funzione ma possiamo restituire un puntatore a un tipo funzione. Allo stesso modo, dobbiamo scrivere il tipo di ritorno come un tipo puntatore; il compilatore non tratterà automaticamente un tipo di ritorno funzione come il tipo puntatore corrispondente. Anche come con i ritorni di array, il modo di gran lunga più semplice per dichiarare una funzione che restituisce un puntatore a funzione è usando un alias di tipo:
// F è un tipo funzione, non un puntatore
using F = int(int*, int);
// PF è un tipo puntatore a funzione
using PF = int(*)(int*, int);
Qui abbiamo usato dichiarazioni di alias di tipo per definire F come un tipo funzione e PF come un tipo puntatore a funzione. La cosa da tenere a mente è che, a differenza di quello che succede ai parametri che hanno tipo funzione, il tipo di ritorno non viene automaticamente convertito in un tipo puntatore. Dobbiamo specificare esplicitamente che il tipo di ritorno è un tipo puntatore:
// OK: PF è un puntatore a funzione;
// f1 restituisce un puntatore a funzione
PF f1(int);
// ERRORE: F è un tipo funzione;
// f1 non può restituire una funzione
F f1(int);
// OK: specifica esplicitamente che
// il tipo di ritorno è un puntatore a funzione
F *f1(int);
Naturalmente, possiamo anche dichiarare f1 direttamente, in questo modo:
int (*f1(int))(int*, int);
Leggendo questa dichiarazione dall'interno verso l'esterno, vediamo che f1 ha una lista di parametri, quindi f1 è una funzione. f1 è preceduto da un asterisco * quindi f1 restituisce un puntatore. Il tipo di quel puntatore stesso ha una lista di parametri, quindi il puntatore punta a una funzione. Quella funzione restituisce un int.
Per completezza, vale la pena notare che possiamo semplificare le dichiarazioni di funzioni che restituiscono puntatori a funzione usando un tipo di ritorno trailing con auto:
auto f1(int) -> int (*)(int*, int);
Uso di auto o decltype per Tipi Puntatore a Funzione
Se sappiamo quale/i funzione/i vogliamo restituire, possiamo usare decltype per semplificare la scrittura di un tipo di ritorno puntatore a funzione. Per esempio, supponiamo di avere due funzioni, entrambe le quali restituiscono un string::size_type e hanno due parametri const string&. Possiamo scrivere una terza funzione che prende un parametro string e restituisce un puntatore a una di queste due funzioni come segue:
string::size_type sommaLunghezza(const string&, const string&);
string::size_type lunghezzaMaggiore(const string&, const string&);
// A seconda del valore del suo parametro string,
// ottieniFcn restituisce un puntatore a sommaLunghezza o a lunghezzaMaggiore
decltype(sommaLunghezza) *ottieniFcn(const string &);
L'unica parte difficile nel dichiarare ottieniFcn è ricordare che quando applichiamo decltype a una funzione, restituisce un tipo funzione, non un tipo puntatore a funzione. Dobbiamo aggiungere un asterisco * per indicare che stiamo restituendo un puntatore, non una funzione.
Esercizi
-
Scrivi una dichiarazione per una funzione che prende due parametri
inte restituisce unint, e dichiara unvectori cui elementi hanno questo tipo puntatore a funzione.Risposta:
#include <vector> using FuncP = int(*)(int, int); std::vector<FuncP> vec; -
Scrivi quattro funzioni che addizionano, sottraggono, moltiplicano e dividono due valori
int. Memorizza puntatori a queste funzioni nel tuovectordell'esercizio precedente.Risposta:
int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int div(int a, int b) { return a / b; } vec.push_back(add); vec.push_back(sub); vec.push_back(mul); vec.push_back(div); -
Chiama ogni elemento nel
vectore stampa il loro risultato.Risposta:
for (const auto& f : vec) { std::cout << f(10, 5) << std::endl; } -
Scrivi una funzione
ottieniOperazioneche prende unchar('+', '-', '*', '/') e restituisce un puntatore alla funzione corrispondente tra le quattro definite sopra.Risposta:
decltype(add) *ottieniOperazione(char op) { switch (op) { case '+': return add; case '-': return sub; case '*': return mul; case '/': return div; default: return nullptr; // o gestire l'errore come preferisci } }