11
Ian
2010

Sa presupunem ca vrem sa scriem un program care sa genereze o secventa de numere aleatoare pe care sa le afiseze in consola, apoi dintre acestea sa selecteze acele numere care sunt pare si sa afiseze aceasta secventa in consola. Un astfel de program ar putea arata asa:

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <iterator>
#include <ctime>

using namespace std;

int randomizer() {return rand()%100;}

struct isodd : unary_function<int,bool>
{
  bool operator() (int x) const {return x%2==1;}
};

int main()
{
	srand((unsigned long)time(NULL));

	// genereaza 10 numere aleatoare
	vector<int> v1, v2;
	generate_n(back_inserter(v1), 10, randomizer);

	// afiseaza numele generate
	for(vector<int>::iterator it = v1.begin(); it != v1.end(); ++it)
	{
		cout << *it << " ";
	};
	cout << endl;

	// elimina numerele impare si copiaza cele ramanse intr-un nou vector
	remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), isodd());

	// afiseaza noua secventa
	for(vector<int<::iterator it = v2.begin(); it != v2.end(); ++it)
	{
		cout << *it << " ";
	};
	cout << endl;
	
	return 0;
}

Un posibil rezultat al rularii acestui program este:

41 67 34 0 69 24 78 58 62 64
34 0 24 78 58 62 64

In Visual Studio 2010 putem rescrie aceasta secventa de cod astfel:

int main()
{
	srand((unsigned long)time(NULL));

	// genereaza 10 numere aleatoare
	vector<int> v1, v2;
	generate_n(back_inserter(v1), 10, [] {return rand()%100;});

	// afiseaza numele generate
	for_each(v1.begin(), v1.end(), [](int i) {cout << i << " ";});
	cout << endl;

	// elimina numerele impare si copiaza cele ramanse intr-un nou vector
	remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) -> bool {return (i%2) == 1;});

	// afiseaza noua secventa
	for_each(v2.begin(), v2.end(), [](int i) {cout << i << " ";});
	cout << endl;
	
	return 0;
}

Dupa cum se vede, codul e mai scurt, nu mai avem nevoie de functia randomizer() si nici de functorul isodd, in schimb am introdus cateva constructii noi:

[] {return rand()%100;}

[](int i) {cout << i << " ";}

[](int i) -> bool {return (i%2) == 1;}

Acestea se numesc lambda si sunt constructii similare celor introduse cu cativa ani in urma in .NET. In C++ o expresie lambda are cateva componente. Sa luam spre exemplu ultima dintre acestea.

  • lambda-introducer: aceasta este partea introductiva, care spune compilatorului ca urmeaza o expresie lambda; aici se specifica si ce variabile si in ce mod (valoare sau referinta) se copiaza din blocul in care expresia lambda este definita (vom vedea acest lucru mai jos).
  • lambda-parameter-declaration: specifica parametrii expresiei lambda.
  • lambda-return-type-clause: specifica tipul la care se evalueaza expresia lambda; aceasta parte este optionala, cel mai adesea compilatorul putand deduce implicit care este tipul expresiei. Este necesara specificare explicita a tipului doar atunci cand tipul valorii returnate este ambiguu. Pentru exemplul anterior specificarea tipului valori returnate (> bool) nu este necesara.
  • compound-statement: reprezinta corpul expresiei lambda.

Iata un exemplu in care compilatorul nu poate deduce tipul returnat, si acesta trebuie specificat explicit:

vector<int> v1;
transform(v1.begin(), v1.end(), v1.begin(), [](int n) {
  if(n > 0)
    return n * 2;
  else 
    return 0;
});

In cazul codului de mai sus compilatorul va arunca doua erori:

1>lambdas.cpp(68): error C3499: a lambda that has been specified to have a void return type cannot return a value
1>lambdas.cpp(70): error C3499: a lambda that has been specified to have a void return type cannot return a value

Exista doua tipuri de expresii lambda: cu si fara stare. Cele pe care le-am vazut in exemplele de mai sus sunt fara stare. Ele nu contin variabile membru. Astfel de lambda sunt introduse cu []. Se pot insa captura variabile din scopul in care este declarata expresia lambda, atat prin valoare cat si prin referinta. In acest caz vorbim de lambda cu stare. Pentru a vedea modurile in care se pot capta variabile sa plecam de la exemplul urmator.

void function()
{
	int a;
	int b;
	int c;

	auto l = [] (int n) {};
}

Exemplele de mai jos arata posibilitatile de capturare a variabilelor a, b si c.

  • [] (int n) {}: nu se captureaza nimic.
  • [=] (int n) {}: se captureaza a, b, si c prin copiere.
  • [a] (int n) {}: se captureaza a prin copiere.
  • [&] (int n) {}: se captureaza a, b, si c prin referinta.
  • [&a] (int n) {}: se captureaza a prin referinta.
  • [a, &b, c] (int n) {}: se captureaza a si c prin valoare si b prin referinta.
  • [=, &b] (int n) {}: se captureaza a si c prin valoare si b prin referinta.
  • [&a, b, &c] (int n) {}: se captureaza a si c prin referinta si b prin valoare.
  • [&, b] (int n) {}: se captureaza a si c prin referinta si b prin valoare.

Dupa cum se observa se pot captura variabile prin copiere si valoare in orice combinatie posibila. Un caz special il reprezinta captarea pointerului this, care se face intotdeauza prin valoare. Nu se poate specifica &this.

In exemplul de mai jos avem definita o clasa Range (cu un minim si maxim) care contine o functie ce filtreaza acele elemente ale unui vector care nu sunt intre limite, returnand un vector nou doar cu elementele din raza de valori.

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <iterator>
#include <ctime>

using namespace std;

template <typename T>
class Range
{
	T m_min;
	T m_max;
public:
	Range(T a, T b): m_min(a), m_max(b){}

	vector<T> Filter(const vector<T>& input)
	{
		vector<T> output;
		remove_copy_if(
			input.begin(), input.end(), 
			back_inserter(output),
			[this](T n) {return !(this->m_min <= n && n <= this->m_max);});

		return output;
	}
};

int main()
{
	srand((unsigned long)time(NULL));
	
	// genereaza o secventa de 10 numere aleatoare
	vector<int> v1;
	generate_n(back_inserter(v1), 10, [] {return rand()%100;});

	// afiseaza numele generate
	for_each(v1.begin(), v1.end(), [](int i) {cout << i << " ";});
	cout << endl;

	Range<int> r(1, 25);

	// filtreaza numerele din intervalul definit
	vector<int> v2 = r.Filter(v1);

	// afiseaza noua secventa
	for_each(v2.begin(), v2.end(), [](int i) {cout << i << " ";});
	cout << endl;
	
	return 0;
}

Un posibil rezultat pentru programul de mai sus este:

45 41 24 7 10 12 98 33 7 14
24 7 10 12 7 14

Sintaxa this->m_min este optionala, se poate scrie direct m_min.

In exemplul de mai sus folosim de 2 ori aceiasi functie lambda pentru afisarea elementelor unui vector. Pentru a evita acest lucru se poate folosi o variabila care sa ia valoarea expresiei lambda si pasa functiei for_each. Pentru tipul acestei variabile se poate folosi cuvantul cheie auto, care in C++0x a primit o noua intrebuintare.

auto print = [](int i) {cout << i << " ";};

// afiseaza numele generate
for_each(v1.begin(), v1.end(), print);
cout << endl;

Urmatorul exemplu arata o functie lambda folosita pentru a calcula suma numerelor dintr-o secventa.

int main()
{
	srand((unsigned long)time(NULL));

	// genereaza o secventa de 10 numere aleatoare
	vector<int> v1;
	generate_n(back_inserter(v1), 10, [] {return rand()%100;});

	// afiseaza numele generate
	for_each(v1.begin(), v1.end(), [](int i) {cout << i << " ";});
	cout << endl;

	// calculeaza suma
	int sum = 0;
	for_each(v1.begin(), v1.end(), [&sum] (int n) {sum += n;});

	// afiseaza suma
	cout << "sum = " << sum << endl;
	
	return 0;
}
57 96 17 43 2 79 24 44 14 93
sum = 469

Variabila sum este capturata prin referinta, astfel incat valoare ei poate fi modificata. Expresia lambda din acest exemplu este transformata de compilator intr-un functor.

class lambda_functor
{
	int& m_sum;

public:
	lambda_functor(int& sum) : m_sum(sum) {}

	void operator() (int n)
	{
		m_sum += n;
	}
};

Astfel, acest cod

int sum = 0;
for_each(v1.begin(), v1.end(), [&sum] (int n) {sum += n;});

devine

int sum = 0;
for_each(v1.begin(), v1.end(), lambda_functor(sum));

Datorita faptului ca functiile lambda sunt transformate de compilator in functori (function objects) acestea pot fi stocate in std::tr1::function. Iata un exemplu:

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
#include <iterator>
#include <string>

using namespace std;

template <typename T>
void apply(const vector<T>& data, const tr1::function<void (T)>& func)
{
	for_each(data.begin(), data.end(), func);
}

int main()
{
	vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);

	apply<int>(v1, [](int n) {cout << n * n << " ";});
	cout << endl;

	vector<string> v2;
	v2.push_back("marius");
	v2.push_back("bancila");
	v2.push_back("codexpert");

	apply<string>(v2, [](string s) {cout << s << " ";});
	cout << endl;
	
	return 0;
}

Pentru alte informatii despre functiile lambda vedeti blogul echipei VC++. Informatii despre calculul lambda in matematica si programare se pot gasi pe wikipedia.