Dopo un mese e mezzo di silenzio, la notizia della definitiva approvazione del nuovo standard internazionale ISO C++ 2011 mi ha spronato a scrivere qualcos’altro in merito. Questa volta mi concentrerò su una funzionalità forse marginale ma senza dubbio molto utile. Le lambda functions.

Chi ha programmato almeno una volta in stile funzionale sa cosa sono le lambda expression: delle espressioni con le quali è possibile definire in loco il corpo di una funzione anonima. In C++ 2011 la sintassi per definire una lambda expression è la seguente:

[capture-list](arguments) -> return_type {body}

Ed ecco un esempio concreto, che usa std::sort per ordinare un vettore in base al modulo degli elementi.

void f()
{
   std::vector<int> v;
   // fill the vector...
   std::sort(v.begin(), v.end(), [](int a, int b) {
      return abs(a) < abs(b);
   });
}

La coppia di parentesi quadre viene chiamata lambda introducer, ed indica l’inizio di una lambda expression. Dopo il lambda introducer si specifica la lista degli argomenti accettati dalla lambda con la solita sintassi tra parentesi tonde. In questo esempio, la lambda che ho scritto non specifica il valore di ritorno, perchè nel caso in cui il corpo contenga una singola istruzione di return, il tipo può essere automaticamente dedotto dal compilatore (in questo caso bool).

Il corpo di una lambda può riferirsi a variabili presenti nello scope che la racchiude. In questo caso, all’interno delle parentesi quadre si può specificare una lista di variabili che verranno catturate dalla lambda. Si può decidere di catturare le variabili per valore o per riferimento. Nel primo caso il valore viene copiato e la lambda ottiene una propria copia della variabile. Nel secondo caso la lambda cattura un reference alla variabile, che può quindi anche essere modificata. Un esempio:

void f(int threshold)
{
   std::vector<int> v;
   int comparisons = 0;
   // fill the vector ...
   std::sort(v.begin(), v.end(),
             [=threshold, &comparisons] (int a, int b) -> bool {
                comparisons++;
                return abs(a) < abs(b) - threshold;
             });
   std::cout << "Array ordinato con " << comparisons << " confronti\n";
}

Com’è facile intuire, il simbolo ‘=’ indica la cattura per valore mentre ‘&’ quella per riferimento.
Non è necessario specificare l’intera lista delle variabili catturate. Specificando solo un singolo ‘=’ o un singolo ‘&’, si decide la modalità di cattura di default, dopodichè il compilatore si arrangerà a catturare tutte le variabili necessarie.

Chi è abituato a programmare in linguaggi funzionali, dove le funzioni sono un entità di prima classe, si starà chiedendo “un’espressione lambda che tipo ha?”.
Rispondere a questa domanda richiede un approfondimento su come sono implementate sotto il cofano. In pratica il discorso è molto semplice, e prevede teoricamente ‘solo’ una riscrittura del codice. Il codice dell’ultimo esempio viene trattato dal compilatore come se fosse riscritto così:

class __fresh_class_name {
   int threshold;
   int &comparisons;
public:
   __fresh_class_name(int t, int &c) : threshold(t), comparisons(c) {}
   bool operator()(int a, int b) const {
      comparisons++;
      return abs(a) < abs(b) - threshold;
   }
};
 
void f(int threshold)
{
   std::vector<int> v;
   int comparisons = 0;
   // fill the vector ...
   std::sort(v.begin(), v.end(), __fresh_class_name(threshold, comparisons));
}

In pratica quindi, una lambda expression crea un’istanza di un function object, il cui tipo viene implicitamente dichiarato dal compilatore. Le variabili catturate sono memorizzate in variabili membro del function object, mentre il corpo finisce nell’overload dell’operator(). Questo approccio ha un vantaggio principale: la totale assenza di qualunque overhead a runtime. Anzi, molto spesso le lambda più semplici possono essere compilate inline generando codice molto efficente.

Il tipo generato dal compilatore per la dichiarazione di una singola lambda expression non è esplicitamente menzionabile nel codice. Oltretutto, diversamente da quello che accade in altri linguaggi, il tipo delle lambda non ha a che fare con il tipo dei parametri e con il tipo di ritorno, ma è specifico e univoco per ogni singola lambda expression.
In altre parole, due lambda con gli stessi parametri e lo stesso tipo di ritorno non hanno lo stesso tipo.
Tuttavia, volendo assegnare una lambda ad una variabile è possibile far dedurre automaticamente il tipo al compilatore con la parola chiave ‘auto’, il cui comportamento è un’altra novità del nuovo standard. Esempio:

void f() {
   auto square = [](int a) { return a * a; };  
   std::cout << square(4) << std::endl;
}

Per passare una lambda come parametro di una funzione invece, come nel caso dell’std::sort usata prima, l’assenza di un tipo ben definito non è un problema. Il codice è esattamente lo stesso di una funzione che si aspetta un function object. Il prototipo di std::sort ad esempio, non è cambiato rispetto al C++03, ed è il seguente (paragrafo 25.4.1.1 del documento ISO):

template<class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

In altri casi è difficile gestire le cose con questo meccanismo. Alcune volte ad esempio potremmo non voler implementare una funzione template. In questi casi torna molto utile una novità introdotta nella libreria standard, ovvero la classe std::function, dichiarata dall’header <functional>, che si comporta come un ‘wrapper’ per oggetti ‘invocabili’ con un certo prototipo, siano essi puntatori a funzione, function objects o lambda expression:

void f() {
   std::function<int(int)> square = [](int a) { return a * a; };
   std::cout << square(4) << std::endl;

L’implementazione di std::function è di per se molto semplice e sfrutta i vararg templates per poter funzionare qualsiasi sia il numero di parametri specificato.

Si chiude qua la mia (quasi) breve panoramica su questa funzionalità, che a mio avviso è una delle più interessanti del nuovo standard. Per poter provare su strada queste non tutti i compilatori sono adatti. Per ora le lambda sono supportate da GCC 4.5 e da Visual C++ 10, mentre non sono ancora implementate in Clang (non ho notizie riguardo ad altri compilatori).

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

 
© 2011 Nicola Gigante Hosted by NetMDM Suffusion theme by Sayontan Sinha