L'overloading dell'operatore ()
, detto operatore di chiamata di funzione, è un interessante caso di studio. Grazie ad esso è possibile implementare un costrutto comunemente impiegato nella programmazione orientata agli oggetti: il funtore (o functor).
In C++, infatti, ogni istanza di una classe dotata di un sovraccarico dell'operatore ()
è detta funtore. Il nome deriva dal fatto che l'invocazione di tale sovraccarico è praticamente indistinguibile dalla sintassi adottata per l'invocazione di una funzione.
I funtori sono comunemente impiegati in C++ per l'implementazione di callback, in sostituzione dei puntatori a funzione tradizionalmente in uso in linguaggio C.
Callback e funtori
Storicamente, la progettazione di librerie generiche e la loro implementazione è sempre stata condizionata alla necessità di estenderne l'uso a tipi definiti dall'utente.
Uno dei costrutti più usati a tale scopo sono le funzioni di callback. In pratica, ogni qualvolta è richiesto di svolgere operazioni specifiche, imprescindibili dalla conoscenza del tipo delle istanze su cui si opera, ci si affida all'esecuzione di una funzione definita dall'utente, che può essere passata come parametro alle funzioni della libreria, ed invocata al momento opportuno.
Un esempio classico è quello che riguarda l'implementazione di algoritmi generici per l'ordinamento di un set di oggetti. I criteri del confronto per stabilire una relazione d'ordine tra elementi possono variare di caso in caso.
Nel listato seguente prendiamo in esame la definizione di un funtore che possa essere impiegato nel confronto di istanze della classe Complex, mediante la valutazione del modulo:
// complex_comparer.h
#ifndef COMPLEX_COMPARER
#define COMPLEX_COMPARER
#include "complex.h"
class ComplexComparer
{
public:
bool operator()(const Complex& a, const Complex& b);
};
#endif // COMPLEX_COMPARER
La classe ComplexComparer include la definizione di un sovraccarico dell'operatore di invocazione a funzione che accetta come argomenti due const reference a istanze della classe Complex e restituisce come risultato un valore booleano pari a true
nel caso in cui il modulo di a sia minore del modulo di b.
#include "complex_comparer.h"
#include <cmath>
bool ComplexComparer::operator()(const Complex& a, const Complex& b)
{
float magA = std::sqrt(a.getReal() * a.getReal() + a.getIm() * a.getIm());
float magB = std::sqrt(b.getReal() * b.getReal() + b.getIm() * b.getIm());
return magA < magB;
}
La modalità d'uso della classe ComplexComparer è mostrata nel listato successivo, in cui una sua istanza ottenuta dall'invocazione diretta del costruttore, è usata come argomento della funzione max()
definita nella libreria standard algorithm.
#include <iostream>
#include <algorithm>
#include "complex.h"
#include "complex_comparer.h"
int main()
{
Complex a(1, 1.5);
Complex b(0.5f, 4);
Complex m = std::max(a, b, ComplexComparer());
std::cout << "max = " << m << std::endl;
return 0;
}
Gli argomenti della funzione max()
sono due oggetti dello stesso tipo a e b, ed un funtore usato come callback che effettua la comparazione tra i due e restituisce un valore booleano se il primo è minore del secondo, come nel caso del sovraccarico dell'operatore ()
definito in ComplexComparer.
In generale, proprio come nel caso di una funzione, è possibile ridefinire l'operatore ()
a seconda delle proprie esigenze, anche molteplici volte, con argomenti che variano per numero e tipo.
Puntatori a funzione, funtori e la libreria functional
La definizione di puntatori a funzione ed il loro uso come callback è spesso ricorrente in molte librerie C, e data la retrocompatibilità di C++ con C, anche molte implementazioni C++ fanno ricorso a tale costrutto.
Tuttavia, l'uso dei funtori presenta notevoli vantaggi, sia a tempo di compilazione che a tempo di esecuzione. Pertanto l'orientamento corrente è quello di relegare l'uso di puntatori a funzione a implementazioni legacy, e in generale esso tende a scomparire nei listati basati sullo standard c++98 o superiore.
Ciò accade in primo luogo perchè la sintassi usata per la definizione di puntatori a funzione non è facilmente leggibile. In secondo luogo, molto spesso, consente di aggirare il controllo statico dei tipi effettuato dal compilatore a tempo di compilazione, trasferendo al contesto dell'invocazione la responsabilità sui tipi usati per gli argomenti o valore restituito, a danno della stabilità generale dell'applicazione.
Inoltre, un puntatore a funzione può essere facilmente riassegnato a tempo di esecuzione, pertanto raramente il compilatore applicherà delle ottimizzazioni come l'inlining della funzione puntata.
Le moderne implementazioni delle librerie standard C++ rendono possibile usare indifferentemente puntatori a funzione e funtori, preservando di fatto la funzionalità di molte applicazioni "legacy".
Tuttavia, ciò è possibile grazie all'uso di costrutti sintattici evoluti come i template e di librerie standard appositamente progettate allo scopo (<functional>).
Entrambi questi argomenti, per la loro vastità e complessità, esulano dall'ambito di questa lezione, e verranno trattati in seguito. L'aspetto rilevante della faccenda consiste nell'essere consapevoli dell'esistenza di un layer di traduzione, molto spesso trasparente al programmatore, come nel caso dei listati qui discussi, che rende possibile la transizione da un costrutto all'altro.
Chiusure funzionali in C++
La definizione di callback non esaurisce l'utilità dei funtori in C++. Uno degli altri ambiti di applicazione è la definizione di chiusure funzionali.
Una chiusura è, in termini generali (e non rigorosi!), l'unione di una funzione e di un insieme di variabili il cui contesto è esterno al corpo della funzione stessa.
Poichè l'ambito di visibilità di tali variabili travalica il corpo della funzione, l'esito di una sua invocazione può cambiare in base allo stato delle variabili.
In C++ i funtori, in quanto oggetti "invocabili" che possono includere al loro interno dati membro, sono spesso associati alla definizione di chiusura, nella misura in cui l'esecuzione dell'operatore ()
può essere condizionata dallo stato dei dati membro dell'istanza stessa.
Questa caratteristica dei funtori è alla base di un altro costrutto avanzato, le espressioni lambda, che sono di fatto una versione sintatticamente "concisa" della definizione di funtore, e che verranno trattate in dettaglio in seguito.