Esempio di Lambda Expression/Function (C++0x)…

Finalmente sono riuscito a mettere le mani sul G++ 4.5, così da poter testare anche su questo compilatore le “lambda function/expression” introdotte nel futuro standard C++0x (cfr. post precedente).

Partiamo subito col dire che le “lambda” sono una new-entry del nuovo standard e, nonostante usarle non sia indispendabile, rappresentano comunque un’aggiunta interessante per una serie di motivi che vedremo immediatamente.

Per chi ha fretta: le lambda sono un modo rapido per definire in fretta delle funzioni-oggetto senza nome, definendole eventualmente proprio dove servono (“in place”), cioè al momento di invocarle.

Dall’ultimo draft dello standard (N3092, Marzo 2010):


5.1.2 Lambda expressions

Lambda expressions provide a concise way to create simple function objects.

Un esempio “canonico”: una funzione f(g()) che accetta come parametro un’altra funzione o un functor g() (“callback”). In un modo o nell’altro, prima di invocare f(), bisognerà aver già definito g().

Le lambda permettono di definire proprio quella g(), dove la si dovrebbe anche invocare.

Sintassi

Devo ammettere che la sintassi è un po’ raccapricc… ehm esteticamente poco piacevole (se paragonata a quelle delle Boost Libs, ad esempio), però ha il pregio di apparire molto simile a quelle delle funzioni scritte in modo “canonico”:

[]( *PARAMETERS* ){ *FUNCTION-BODY* };

La parte strana non sono nè i parametri nè il corpo della funzione, ma quella coppia di parentesi quadre nota come lambda-introducer, che serve appunto a marcare l’inizio della “funzione senza nome“.

Probabilmente scritta come una “funzione”, l’analogia è più evidente:

[]( *PARAMETERS* )
{ 
	*FUNCTION-BODY*
};

In realtà il lambda introducer è stato scelto perchè consente di specificare il modo di accesso alle variabili esterne, permettendo quindi di ottenere delle funzioni in grado di operare su variabili definite nell’”ambiente circostante” (closure“, “chiusura” in italiano).

Vediamo rapidamente i vantaggi delle lambda.

1. Funzioni dentro funzioni e closure

Con la sintassi “standard” non è possibile definire delle funzioni dentro altre funzioni, questo perchè il C++ non dispone di first-class function, cioè le funzioni non sono oggetti a differenza di altri linguaggi (questo ovviamente a meno di creare dei functor che sono oggetti a tutti gli effetti).

Le lambda colmano questa “lacuna” (alcuni la percepiscono così): le lambda non solo possono essere definite all’interno di funzioni scritte con la sintassi “canonica” ma possono perfino accedere alle (“catturare”) variabili locali delle funzioni che le contengono.

In questo caso si parla di “closure” e il lambda-introducer permette di definirne l’eventuale accesso selettivo alle variabili esterne.

Un ottimo esempio tratto dal blog del Team di Visual C++:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>

using namespace std;

int main() {

    vector<int> v;
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    int sum = 0;
    int product = 1;
    int x = 1;
    int y = 1;

    for_each(v.begin(), v.end(), [=, &sum, &product](int& r) mutable {
        sum += r;
        if (r != 0) {
            product *= r;
        }

        const int old = r;

        r *= x * y;
        x = y;
        y = old;
    });

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;

    cout << "sum: " << sum << ", product: " << product << endl;
    cout << "x: " << x << ", y: " << y << endl;
}

In questo caso il l’introducer della prima lambda, [=, &sum, &product], mostra le sue potenzialità: due variabili verranno “catturate” e usate by reference (il simbolo “&” denota questo fatto) mentre tutto il resto verrà “catturato” by value (simbolo “=”).

Un introducer vuoto, [], indica invece che la lambda non “catturerà” nessuna variabile esterna.

L’esempio include anche la parola-chiave mutable, che rende “modificabile” la lambda: senza questa keyword, le lambda sono non-mutabili per default, cioè const.

Sempre dal draft:

The closure type for a lambda-expression has a public inline function call operator (13.5.4) whose parameters and return type are described by the lambda-expression’s parameter-declaration-clause and trailing return-type respectively. This function call operator is declared const (9.3.1) if and only if the lambda expression’s parameter-declaration-clause is not followed by mutable.

In altre parole una lambda viene “tradotta” nell’equivalente di un functor il cui operator() è marcato public inline e const.

2. Funzioni-oggetto

Come detto poc’anzi le lambda sono degli “oggetti”, delle funzioni-oggetto scritte in modo particolare.


The evaluation of a lambda-expression results in a prvalue temporary (12.2). This temporary is called the closure object. A lambda-expression shall not appear in an unevaluated operand (Clause 5). [ Note: a closure object behaves like a function object (20.8).—end note ]

Ciò permette di trattare le lambda come dei functor: è possibile salvarle in variabili e perfino usarle come parametri di default!

Sempre dall’ultimo draft:

auto x1 = [](int i){ return i; }; // OK: return type is int

[...]

void f2() 
{
	int i = 1;
	void g1(int = ([i]{ return i; })()); // ill-formed
	void g2(int = ([i]{ return 0; })()); // ill-formed
	void g3(int = ([=]{ return i; })()); // ill-formed
	void g4(int = ([=]{ return 0; })()); // OK
	void g5(int = ([]{ return sizeof i; })()); // OK
}

Nella prima parte dell’esempio, la lambda viene “salvata” in una variabile che poi può essere riusata più avanti con la sintassi di una funzione (es: x1(100);).

Nella seconda parte vengono dichiarate delle funzioni che usano delle lambda come parametri di default (si noti il simbolo “=” ed il lambda-introducer vuoto, usato per indicare che i paremtri verranno passati per copia e non by-reference).

3. Codice più compatto

La possibilità di definire funzioni dove servono è estremamente comoda, soprattutto nel caso di codice che fa abbondante uso di “callback”: se per ogni chiamata f() è necessario specificare la sua apposita funzione “standalone” g(), da passarle come parametro, c’è il rischio di avere miriadi di piccole funzioni create appositamente per quello scopo. Questo tendere a rendere il codice anche più leggibile perchè trovo la “funzione” esattamente dove la dovrei usare e non in separata sede.

4. Possibilità di maggiori ottimizzazioni

Una “lambda” è in pratica una funzione anonima che può essere definita ed usata solo in un punto preciso del codice: in quel caso il compilatore ha quindi molte più informazioni per ottimizzare il codice perchè ha la certezza che la “funzione” sarà invocata solamente una volta in quel punto.

Sempre dall’ultimo draft:


The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed nonunion class type — called the closure type — whose properties are described below. This class type is not an aggregate (8.5.1).

Codice di esempio

Il seguente codice riprende l’esempio di Wikipedia e mostra alcuni usi delle lambda.

Ho testato il codice sia col G++ 4.5 che con Visual C++ 2010 su Windows.

Spero vi piaccia.

Ciau!

/**
 *	FILE      : LambdaExample.cpp
 *	AUTHOR    : Gian Paolo "JP" Ghilardi (http://rejex.wordpress.com)
 *	LICENSE   : released under the terms of GPL v2.0 ("only")
 *	COMPILE   : g++ -Wall -Winline -pedantic -std=c++0x
 *	            LambdaExample.cpp -o LambdaExample.cpp
 *	PURPOSE   : simple C++0x example showing the new lamba-expression support
 *
 *	TESTED ON :
 *	- Windows XP SP3, x86 32-bit, G++ 4.5.0
 *	- Windows XP SP3, x86 32-bit, Visual C++ 2010
 *
 *	REFERENCES:
 *	[1]: http://en.wikipedia.org/wiki/Lambda_expression
 *  [2]: http://en.wikipedia.org/wiki/C%2B%2B0x#Lambda_functions_and_expressions
 *  [3]: http://en.wikipedia.org/wiki/Closure_%28computer_science%29
 *	[4]: http://gcc.gnu.org/gcc-4.5/cxx0x_status.html
 *	[5]: http://en.wikipedia.org/wiki/C%2B%2B0x#Type_inference
 *	[6]: http://en.wikipedia.org/wiki/C%2B%2B0x#Initializer_lists
 *	[7]: http://en.wikipedia.org/wiki/C%2B%2B0x#Uniform_initialization
 *	[8]: http://www.cplusplus.com/reference/algorithm/for_each/
 */

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <vector>

using namespace std;

namespace
{
	int gtotal = 0;
	void sum (int x) // used in the first for_each() [8]
	{
		gtotal += x;
	}
}

/**
 * This simple example shows a lamdda expression instead of a "standard" function.
 * That may allow for further compiler optimization as the lambda is a
 * "in place"-defined anonymous function used only in that position.
 *
 * So the new lambda-expression syntax allows for defining "function inside functions",
 * something that's forbidden with the "standard" function syntax.
 *
 * Moreover the example show lambdas "capturing" and using variables from the
 * surrounding environment (by reference) => "closure" [3]
 */
int main(int argc, char ** argv)
{
	cout << "\nC++0x EXAMPLE:" << endl;
	cout << "Simple example showing C++0x lambda functions and expressions" << endl << endl;

    vector<int> v = { 1, 2, 5, 7 }; // [6, 7] (doesn't work in VC2010. Use push_back())

    // 1. without using a lambda (using an external function)
    for_each(v.begin(),       // 1st param
             v.end(),         // 2nd param
             sum              // 3rd param: function
    );
    cout << "1. No lambda: " << gtotal << endl;


    // 2. with a lambda expression
    int ltotal = 0;
    for_each(v.begin(),        // 1st param
             v.end(),          // 2nd param
             [&ltotal](int x)  // 3rd param: lambda
             {
                 ltotal += x;  // capturing variable "ltotal" => "closure" [3]
             }
    );
    cout << "2. With lambda: " << ltotal << endl;


    // 3. with a stored lambda expression
    int ltotal2 = 0;
    auto lambda = [&ltotal2](int x) // stored lambda
		          {
					  ltotal2 += x;   // capturing variable "ltotal2" => "closure" [3]
			      };

	for_each(v.begin(),       // 1st param
			 v.end(),         // 2nd param
			 lambda           // 3rd param: stored lambda
	);
    cout << "3. With stored lambda: " << ltotal2 << endl;


    // 4. with a lambda expression defining variables
    for_each(v.begin(),       // 1st param
			 v.end(),         // 2nd param
             [&v](int x)      // 3rd param: lambda expression
             {
				static int  ltotal3 = 0;    // defining a variable
				static auto it = v.begin(); // defining a variable [5]

                ltotal3 += x;
                if(++it == v.end()) // if we've reached the last item...
                	cout << "4. With lambda defining variables: " << ltotal3 << endl;
             }
	);

    return EXIT_SUCCESS;
}

Il codice in esecuzione

C++0x example: lambda functions/expressions

Contrassegnato da tag , , ,

5 thoughts on “Esempio di Lambda Expression/Function (C++0x)…

  1. Martino scrive:

    ciao Paolo, molto interessante questo articolo complimenti.

    Concordo con te quando dici che la sintassi delle espressioni lambda sia abbastanza contorta ma spero che con l’abituarsi nel leggere/scriverle questa sensazione sparisca :)
    L’esempio del foreach mi piace molto e la possibilità di poter “catturare” le varibiali fuori dal suo scope è molto potente. L’equivalente era l’utilizzo di free function/member function con il binding dei parametri. Una pratica che costringe di scrivere più codice e a volte non raggiungere neanche i risultati …ad esmepio ad oggi non si può fare il binding di parametri reference pena errore “reference to reference” . Si, si può usare i functor ma in queste circostanze è codice in più da scrivere.

    La mia “paura” è questa. Non è che si rischia di scrivere N lambda functions dove serve creando molte ripetizioni al codice con tutti i problemi annessi?

  2. contezero74 scrive:

    Ciao JP,
    articolo interessante che spiega bene cosa sono e a cosa possono servire le lambda function… di questo ti do atto ;)
    Io continuo ad essere molto scettico sulla loro reale utilità. Ad esempio, il fatto che il compilatore “potrebbe” fare maggiori ottimizzazioni è, IMHO, abbastanza utopico: già con la dichiarazione inline (che concettualmente è molto più semplice) non sempre le ottimizzazioni avvengono come uno spererebbe… senza considerare le altre variabili di questo vasto argomento “le ottimizzazioni” (piccola divagazione che spero di fare presto su un mio post).
    Ma non ti preoccupare, il mio scetticismo non dipende dalle lambda in quanto tali: non ho mai apprezzato le funzioni anonime neppure in Java ;)

    cheers

  3. jp scrive:

    @martino: le lambda sono un’aggiunta, da un lato puro “syntactic sugar” per scrivere “funzioni-oggetto in modo conciso”. Ma offrono dei vantaggi innegabili rispetto alle funzioni “normali” come appunto la possibilità di essere vere e proprie “closure“. Domani pubblicherò un secondo piccolo post in cui mostrerò un esempio di utilizzo pratico delle lambda, qualcosa che non è così immediatamente fattibile con le “funzioni” normali. Non che non sia possibile, ma non in modo così elegante e conciso, appunto.

    E ce ne sono di cose che si potrebbero dire ed esempi da portare…

    Quanto al rischio di cui parli, a dire il vero non ci credo troppo. Non perchè non esista (ognuno è libero di fare quello che vuole, compreso il farsi male), ma perchè se uno stesso codice viene usato in più posti, a quel punto vale la pena “inscatolarlo” in una funzione dedicata, non ripeterlo così com’è in più lambda. Sbaglio? :)

    @contezero: ci sono un sacco di cose che non mi piacciono del C++ (molte di derivazione C) ma sfortunatamente questo non ha impedito al Comitato C++ di inserirle così come sono… :P

    Scherzi a parte, i compilatori non sono più/affatto scemi, questo è chiaro ed è lecito supporre che più indicazioni certe dai loro, più loro possono attivare ottimizzazioni anche spinte.

    Il draft dello standard dice che

    “[...] The closure type for a lambda-expression has a public inline function call operator”

    ossia, se possibile, ogni lambda deve essere se possibile “inlined” automaticamente. Poco o tanto che sia, superfluo o meno, è un invito esplicito all’ottimizzare l’ottimizzabile da parte dello standard.

    Mi spiace davvero che il G++ 4.5 ed il Visual C++ 2010 non includano ancora più feature C++0x perchè, ad esempio, l’accoppiata thread/future + lambda promette terribilmente bene ed appare veramente utile ed interessante (e, se vogliamo, raggiunge in pieno il C# come feature-set, almeno da questo punto di vista)…

    PS: sto leggendo pian pianino il draft e ci sono cose veramente sfiziose, tipo l’equivalente diretto ed esplicito per le classi “sealed” del C#… (tema che avevo trattato tempo fa)

    Ciao & grazie ad entrambi per essere passati! ^^

  4. NeXuS scrive:

    Bell’articolo, anche se continuo a ritenere le scelte sintattiche per il C++ terribili.

    Sull’ottimizzazione concordo con Contezero: mentre sono sicuramente un passo avanti rispetto ai puntatori a funzione (ma anche i “functor” in sintassi “semplice” lo sono), il fatto che le lambda abbiano un inline implicito vuol dire poco… la keyword inline, come sai meglio di me, e’ solo un suggerimento del compilatore.

    Gia’ mi vedo la compilazione (assolutamente fittizia e probabilmente manco verra’ fatta cosi’):
    [](){…}
    PREPROCESSORE – “Toh, una lambda. Mo’ gliela metto giu’ bene.”
    inline mangled_unique_name(){…}
    COMPILATORE – “inline… uhm…. ma anche no!” ;)

  5. jp scrive:

    @Nexus: non metto in dubbio che “inline” sia solo un “hint” e nulla più. Però ho imparato che se lo standard per qualcosa scrive “unspecified” o “undefined” è una cosa, se dice “shall” è un’altra cosa.

    Peggio se in una versione dello standard viene “rapidamente” introdotto qualcosa lasciandolo quindi alla mercè degli implementatori rispetto ad una “new-entry” ben definita fin dalla sua prima comparsa. :D

    Il fatto che le lambda siano “by default” (e a meno di cambiamenti espliciti) marcate come “public inline … const” non è cosa da poco (*)

    Dal draft:

    The closure type for a lambda-expression has a public inline function call operator (13.5.4) whose parameters and return type are described by the lambda-expression’s parameter-declaration-clause and trailing return-type respectively. This function call operator is declared const (9.3.1) if and only if the lambda expression’s parameter-declaration-clause is not followed by mutable.

    Apprezzo che abbiamo scelto proprio quella forma (di solito devi mettere esplicitamente const in fondo al prototipo delle funzioni, qui invece è il default) perchè se la lambda è garantita “const”, usarla in un thread mi pare un po’ più sicuro…

    Ciau!

    (*): approfitto dell’occasione per editare il post e chiarificare bene la keyword “mutable” perchè mi rendo conto che quel paragrafo, scritto così, è un bel po’ ambiguo (e il link che citavo amplificava questo fatto)…

Lascia un Commento

Fill in your details below or click an icon to log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Log Out / Modifica )

Foto Twitter

You are commenting using your Twitter account. Log Out / Modifica )

Foto di Facebook

You are commenting using your Facebook account. Log Out / Modifica )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 259 other followers