Lo header file functional
della libreria STL contiene la definizione di classi e funzioni template finalizzate alla gestione omogenea di funzioni, funtori, ed espressioni lambda.
Queste entità sono molto simili da un punto di vista semantico, tuttavia sono elementi del linguaggio di tipo differente.
Sotto questo aspetto, il linguaggio C++ è un caso particolare. Progettato per essere compatibile con il linguaggio C, ha seguito un'evoluzione incentrata sulla definizione di politiche statiche per il controllo dei tipi, cercando di demandare al compilatore, quanto più possibile, la responsabilità di verificare la correttezza formale delle istruzioni di un programma.
Le revisioni più recenti dello standard hanno ampliato notevolmente la capacità espressiva e semantica del C++, contribuendo a rimarcare ulteriormente le differenze con il linguaggio C.
Tuttavia, poiché il retaggio di quest'ultimo è una parte fondamentale di C++, lo standard include nella specifica del linguaggio alcune librerie di utilità, come functional
, che fungono da layer di traduzione per i costrutti di C caratterizzati da una spiccata dissonanza con la filosofia del C++ moderno.
Questa politica di incapsulamento o wrapping consente di conciliare alcuni aspetti in apparenza contraddittori del linguaggio, e la conoscenza e l'impiego di tali strumenti è parte delle buone prassi di programmazione in C++.
Il tipo di una lambda
Il tipo di una lambda dipende dall'implementazione delle librerie dello standard. In quanto tale, può variare da una piattaforma all'altra.
Negli esempi visti in precedenza, si è aggirato il problema della definizione del tipo di una espressione lambda facendo ricorso all'uso dello specificatore auto.
Tuttavia, questa via non è applicabile in tutti i casi. Ad esempio, supponendo di voler definire una funzione che accetta come parametro un callback è necessario dichiararne il tipo nella firma della funzione.
In linguaggio C, si può ricorrere all'uso di puntatori a funzione. Essi sono variabili che contengono indirizzi di memoria che puntano a sezioni di codice eseguibile invece che a dati. Come per le variabili ordinarie, i puntatori a funzione hanno un proprio tipo. Quest'ultimo identifica il tipo del valore di ritorno, il numero ed il tipo degli argomenti della funzione puntata.
Tutte le funzioni che condividono la stessa firma possono essere referenziate dallo stesso tipo di puntatori a funzione. Tuttavia i puntatori a funzione possono essere associati a funzioni libere, ma in generale non a funtori o espressioni lambda con una clausola di cattura non vuota. Questi ultimi sono infatti istanze di classe con il proprio tipo.
Per garantire maggiore flessibilità, la classe template std::function
consente di incapsulare le varie entità invocabili del linguaggio sotto un'unica sintassi. Ciò consente di definire funzioni che accettano come argomento funzioni libere, espressioni lambda o funtori in modo del tutto indifferente.
La sintassi per definire un oggetto invocabile mediante std::function
è la seguente:
std::function<tipo_di_ritorno (lista_dei_tipi_degli_argomenti)>
Ad esempio, std::function<void (int)>
definisce il tipo di un oggetto invocabile che accetta un unico parametro di tipo intero e non ha valore di ritorno.
Nel listato seguente la classe std::function
viene usata per qualificare il tipo del callback usato dalla funzione invokeCallback. Con la sintassi std::function<float(float,float)>
si specifica che l'argomento di nome callback può essere indifferentemente un puntatore a funzione, un funtore o un'espressione lambda la cui firma sia compatibile con quella fornita.
Nella funzione main, la funzione invokeCallback è infatti impiegata con argomenti di tipo eterogeneo, grazie all'incapsulamento operato da std::function.
#include <iostream>
#include <functional>
/*
* Funzione libera
*/
float function1(float a, float b)
{
return a + b;
}
/*
* Classe con sovraccarico dell'operatore ()
*/
class Functor
{
public:
float operator() (float a, float b)
{
return a + b;
}
};
/*
* Funzione che ha un oggetto invocabile definito con std::function tra i suoi argomenti
*/
void invokeCallback(float arg1, float arg2, std::function<float(float,float)> callback)
{
if (callback != nullptr) // sovraccarico dell'operatore != per prevenire l'uso di puntatori a funzione nulli
{
float value = callback(arg1, arg2);
std::cout << "Value = " << value << "\n";
}
}
int main()
{
invokeCallback(1.5f, 1.0f, function1); // funzione
invokeCallback(1.5f, 1.0f, Functor()); // funtore
invokeCallback(1.5f, 1.0f, [] (float a, float b) { return a + b; }); // lambda
return 0;
}
Si osservi inoltre che la classe std::function
è dotata di un sovraccarico dell'operatore !=
per consentire di prevenire la dereferenziazione di puntatori a funzione nulli.
Differenze con i puntatori a funzione del linguaggio C
Un ulteriore vantaggio dell'uso di std::function
consiste nel garantire l'osservanza delle politiche di controllo statico dei tipi.
Si consideri, ad esempio, il seguente listato in linguaggio C in cui la definizione dell'argomento callback è fatta con un puntatore a funzione tradizionale. Si osservi che in questo caso il compilatore C emette soltanto un warning per l'invocazione di invokeCallback con un argomento di tipo incompatibile (la funzione func1).
#include <stdio.h>
int func1(int a)
{
return a * 2;
}
float func2(float a, float b)
{
return a + b;
}
void invokeCallback(float arg1, float arg2, float (*callback) (float, float))
{
if (callback != NULL)
{
float value = callback(arg1, arg2);
printf("Value = %f\n", value);
}
}
int main()
{
invokeCallback(1.5f, 1.0f, func1); // solo un warning! :(
invokeCallback(1.5f, 1.0f, func2);
return 0;
}
L'esecuzione di questo listato dà luogo ad un comportamento indefinito.
Il medesimo programma, tradotto in linguaggio C++ moderno, è mostrato nel listato seguente. In questo secondo caso, l'uso improprio di func1 come argomento di invokeCallback è correttamente segnalato mediante un errore di compilazione.
Si noti che in questo caso il controllo statico dei tipi consente di prevenire facilmente quello che potrebbe essere un semplice
errore di battitura (in effetti i nomi func1 e func2 differiscono per un solo carattere).
#include <iostream>
#include <functional>
int func1(int a)
{
return a * 2;
}
float func2(float a, float b)
{
return a + b;
}
void invokeCallback(float arg1, float arg2, std::function<float(float,float)> callback)
{
if (callback != nullptr)
{
float value = callback(arg1, arg2);
std::cout << "Value = " << value << "\n";
}
}
int main()
{
std::cout << "\nC++ Example\n";
invokeCallback(1.5f, 1.0f, func1); // errore di compilazione!!
invokeCallback(1.5f, 1.0f, func2); // ok
/*
* Forzando un cast sul parametro callback è possibile osservare
* gli effetti del retaggio del linguaggio C in C++: questa istruzione
* è sintatticamente valida, tuttavia l'uso dell'operatore di cast in stile C
* va contro la prassi di buona programmazione in C++.
*/
invokeCallback(1.5f, 1.0f, (float (*) (float, float)) func1); // non ok.
return 0;
}
Ciò nonostante, si osservi anche che, abbandonando le buone prassi di programmazione C++, è sempre possibile ricadere in questo tipo di errori.
Nella terza e ultima invocazione della funzione invokeCallback, l'argomento passato come callback è forzatamente convertito nel tipo atteso dalla funzione. In questo caso, non viene emesso alcun messaggio di errore e si ricade nello scenario di comportamento indefinito.
Tuttavia, l'uso dell'operatore di casting in style C difficilmente è un errore di battitura, pertanto l'uso degli strumenti più evoluti del linguaggio consente, quasi sempre, di compensare le sue eventuali mancanze.