Menu
×
Menu
Możliwość komentowania Funkcje Lambda w języku C++ – poznaj możliwości nienazwanych funkcji lambda wykorzystując C++0x oraz Boost.Lambda została wyłączona

Funkcje Lambda w języku C++ – poznaj możliwości nienazwanych funkcji lambda wykorzystując C++0x oraz Boost.Lambda

Opublikowane przez | 14 sierpnia 2012 | felieton, wydarzenia

Zmorą każdego programisty, korzystającego na co dzień z dobrodziejstw biblioteki standardowej języka C++, jest potrzeba pisania malutkich funkcji – predykatów, sterujących przebiegiem algorytmów, co powoduje naszpikowanie kodu malutkimi klasami realizującymi trywialne operacje. Dodatkową wadą takiego rozwiązania, jest oddzielanie implementacji od miejsca wywołania – co znacznie utrudnia modyfikacje i konserwacje kodu. Jednym z przykładowych rozwiązań tego problemu są funkcje definiowane w miejscu wywołania. Jednakże do czasu pojawienia się nowego standardu języka C++0x, wyrażenia lambda nie były wspierane przez sam język – programista musiał uciekać się do zewnętrznych bibliotek, np. Boost.Lambda. Niniejszy artykuł stanowi wprowadzenie do świata funkcji lambda, których implementacje będą realizowane zarówno przy użyciu nowych mechanizmów standardu C++0x jak i biblioteki Boost.Lambda.

Powinieneś wiedzieć:

  • Język C++
  • Biblioteka standardowa C++

Dowiesz się:

  • Co to są funkcje Lambda ?
  • Jak definiować funkcje lambda w języku C++?

Przykład zamiast wstępu
Możliwości funkcji lambda przy rozwiązywaniu codziennych problemów najlepiej przedstawić jest na przykładzie. Wyobraźmy sobie, że musimy posortować (malejąco) wektor zawierający elementy typu integer. Programista nie znający funkcji lambda, spróbowałby napisać własną funkcję sortującą, bądź skorzystałby z algorytmu biblioteki standardowej „std::sort”. Jednakże „sort” wymaga podania funkcji realizującej porównywanie elementów. Jesteśmy więc zmuszeni napisać takową funkcje. A co w przypadku, gdy kryteria sortowania zmieniałyby się zależnie od sytuacji ? Bylibyśmy skazani na napisanie wielu predykatów. Na szczęście mamy funkcje lambda. Na Listingu 1 przedstawiłem, jak do tego zadania wykorzystać własnoręcznie napisany funktor, funkcje lambda, używając biblioteki Boost.Lambda, oraz nowe mechanizmy języka, zdefiniowane w standardzie C++0x. Podejścia klasycznego – tj. z własnym funktorem nie trzeba chyba nikomu tłumaczyć. Jednakże wywołania lambda, zwłaszcza wariant wykorzystujący bibliotekę Boost, wydawać się mogą nieco magiczne, wszystko to dzięki szablonowej naturze biblioteki Boost.Lambda. Kompilator wykona za programistę cała czarną robotę i sam wygeneruje odpowiedni funktor. Bardziej przyjazną formę ma wyrażenie według nowego standardu C++0x. Jest to po prostu funkcja napisana w miejscu, gdzie normalnie byłaby wywołana. Proste, nieprawdaż? Podsumowując ten krótki wstęp: oba podejścia realizują ten sam cel, pozwalają zdefiniować funkcję w miejscu wywoływana. Wszystkich, którzy chcieliby się dowiedzieć więcej o funkcjach lambda, zapraszam do dalszej części artykułu, w której omówię zarówno Boost.Lambda jak i wyrażenia C++0x.

Boost.Lambda
Boost.Lambda jest biblioteką autorstwa duetu Jaakko Järviego oraz Gary’ego Powella, oraz wchodzi w skład bibliotek Boost. Już sama przynależność do rodziny Boost obliguje do niezawodności oraz czyni ja wartą poznania. Zatem do dzieła.
Symbole zastępcze(ang. Placeholders)
Przed omówieniem biblioteki Boost.Lambda należy wyjaśnić pojęcie symboli zastępczych, bez których nie sposób się obejść w świecie wyrażeń lambda. Reprezentują one argumenty, które zostaną przekazane do końcowego obiektu funkcyjnego, wygenerowanego przez kompilator. Boost.Lambda udostępnia trzy predefiniowane symbole zastępcze(_1, _2, _3), jednak nic nie stoi na przeszkodzie, aby zdefiniować swoje. Aby czytelnik lepiej zrozumiał koncepcję symboli zastępczych, weźmy na warsztat przykład z Listingu 1. Wygenerowana funkcja będzie miała postać:
template bool foo(T a, T b) { return a > b; }
Podczas działania programu w miejsce _1 zostanie wstawiony element bieżący wektora, natomiast w miejsce _2 element kolejny. Gdybyśmy zdefiniowali wyrażenie jako (_2 > _1), kompilator wygenerował by następująca funkcje:
template bool foo(T a, T b) { return b > a; }
Warto też dodać, że liczba symboli zastępczych narzuca kompilatorowi ilu argumentowy funktor ma wygenerować, wiec wyrażenie lambda, nie zawierające ani jednego symbolu zastępczego, wygeneruje bezargumentowy obiekt funkcyjny, zaś wyrażenie zawierające symbole _1, _2, _3 wygeneruje trójargumentowy funktor.

Operatory
Na wyrażenie Boost.Lambda mogą składać się dowolne, dające się przeciążać, operatory języka C++. Jednak jak mówi stare przysłowie programistów – „ jedna linia kodu wyraża więcej niż tysiąc słów” – dlatego na Listingu 2 przedstawiłem kilka przykładowych wyrażeń Lambda. Znajdziecie tam ich porównanie z algorytmami biblioteki standardowej, wraz z krótkimi opisami.
Wiązanie wyrażeń lambda z funkcjami
Wyrażenia lambda, składające się z operatorów języka C++, dają ogromne możliwości, ale może się zdarzyć, że są nie wystarczające, bądź mamy już gotową funkcję realizującą część zadania, które chcielibyśmy opisać wyrażeniem lambda. Z pomocą przychodzi mechanizm wiązania boost::lambda::bind. Funkcja, wiązana z wyrażeniem lambda, może przyjmować od zera do dziewięciu argumentów, które możemy przekazać bezpośrednio, bądź za pomocą symboli zastępczych. Podczas wiązania możemy użyć maksymalnie trzech symboli zastępczych. Listing 3 przedstawia przykładowe wiązanie wyrażenia lambda z zewnętrzną funkcją. Wyrażenie to przypisze do każdego elementu wektora wynik zwrócony przez zewnętrzną funkcję ‘foo’, od którego zostanie odjęta liczba 12. Parametrami funkcji jest jeden symbol zastępczy, zmienna „a” oraz liczba 5. Dodatkowo każdy element wektora zostanie wypisany na standardowe wyjście. Przykład wydawać się może trochę oderwany od rzeczywistości, jednak celem była prezentacja możliwości, jakie daje programiście mechanizm wiązania „boost::lambda::bind”.

Podsumowanie Boost.Lambda
Boost.Lambda z powodzeniem realizuje koncepcję wyrażeń lambda, jednak wielu programistów, zwłaszcza tych mniej doświadczonych, może uznać ją za trudną i skomplikowaną. Warto jednak opanować używanie tej biblioteki chociażby na podstawowym poziomie i nauczyć się swobodnie definiować proste wyrażenia oparte na operatorach. W dalszej części artykułu przedstawię inną koncepcję wyrażeń lambda, wprowadzoną jako element języka C++ wraz z nowym standardem C++0x.

Funkcje lambda w C++0x
Wraz z nowym standardem, C++ zyskał natywną obsługę wyrażeń lambda. Składniowo wyrażenia te przypominają definicje zwykłych funkcji, przez co wydają się być bardziej intuicyjne, nawet dla niedoświadczonych programistów. Składnia wyrażenia została przedstawiona poniżej:
[zmienne przekazane do wyrażenia] (argumenty wyrażenia) -> typ zwracany{ ciało wyrażenia }(argumenty)
Typ zwracany może zostać pominięty, jeżeli na wyrażenie lambda składa się jedynie pojedyńcza instrukcja return.

Zmienne przekazane do wyrażenia
Funkcje lambda mogą odnosić się do zmiennych zadeklarowanych poza samym wyrażeniem. Zmienne, które mamy zamiar przekazać do wyrażenia lambda, muszą zostać zadeklarowane w nawiasach kwadratowych –„[]” – na początku wyrażenia. Udostępnione są także modyfikatory, które określają, czy zmienna jest przekazana przez referencję bądź wartość. Aby nie zanudzać czytelnika suchą teorią, na Listingu 4 przedstawiłem przykłady wyrażeń, do których przekazywane są parametry w różnych kombinacjach.

Argumenty wyrażeń lambda oraz typy zwracane
Wyrażenia lambda, tak jak i zwykłe funkcje, mogą posiadać argumenty, które zostaną przekazane do funkcji podczas działania programu. Należy także pamiętać, że obowiązują nas te same zasady, jak przy argumentach funkcji. W przypadku, gdy wyrażenie składa się z więcej niż pojedyńczej instrukcji „return”, należy określić typ wartości zwracanej. Na Listingu 5 przedstawiłem kilka przykładów deklaracji argumentów wraz z niedozwolonymi przypadkami.
Przechowywanie wyrażeń lambda do późniejszego wywołania

Na zakończenie warto dodać, że funkcje lambda, zarówno Boost.Lambda jak i C++0x, można przypisać do odpowiednio zadeklarowanego wrappera (wskaźnika na funkcję), bądź zmiennej ‘auto’ (z arsenału C++0x), aby można było użyć ich wielokrotnie, bez potrzeby definiowania wyrażenia za każdym razem. Na Listingu 6 zaprezentowałem wszystkie wymienione sposoby. Jako wrappera użyłem Boost.Function.

Podsumowanie
Wyrażenia lambda, wprowadzone wraz z nowym standardem, są intuicyjne i proste w użyciu, jednak wymagają najnowszych kompilatorów, co może stanowić przeszkodę nie do przeskoczenia dla wielu programistów. Na szczęście istnieją doskonałe alternatywy, takie jak opisana Boost.Lambda.
Wyrażenia lambda, niezależnie od implementacji, są doskonałym narzędziem w rękach doświadczonego programisty. Używane we właściwy sposób pozwalają zaoszczędzić czas, który poświecilibyśmy na pisanie trywialnych funktorów bądź konserwacje kodu. Należy jednak pamiętać, że czasami „mając w rękach młotek, każdy problem wygląda jak gwóźdź”, a zatem nadużywanie funkcji lambda do niczego dobrego prowadzić nie może. Ale spokojnie – wraz z doświadczeniem przyjdzie potrzebne wyczucie.

Listing 1. Rozwiązanie problemu sortowania wektora z użyciem wyrażeń lambda

//podejście klasyczne
class comparator
{
public:
	bool operator()(int a, int b) const
	{
		return a > b;
	}
};
std::sort(v.begin(), v.end(),comparator());
//Boost.Lambda
using boost::lambda::_1;
using boost::lambda::_2;
std::sort(v.begin(), v.end(), _1 > _2);
//standard C++0x
std::sort(v.begin(), v.end(), [](int a, int b) {return a > b;});

Listing 2. Przykładowe wyrażenia lambda

/*poniższa linia kodu wypisze na standardowym wyjściu wszystkie elementy wektora v*/
std::for_each(v.begin(), v.end(), std::cout << _1);

/*pierwsze 5 elementów wektora zostanie pomnożone przez siebie*/
std::for_each(v.begin(), v.begin() + 5, _1 = _1 * _1);

/*algorytm find_if zwróci iterator na element, którego iloczyn z liczba 2 jest równy 8, bądź iloczyn z liczba 3 jest równy 12*/
std::find_if(v.begin(), v.end(), (_1 * 2 == 8 || _1 * 3 == 12));

Listing 3. Przykładowe wiązanie wyrażenia lambda z zewnętrzna funkcją

int foo(int a, int b, int c)
{
	return (a * b) + c;
}
int a = 2;
std::for_each(v.begin(), v.end(), std::cout<<(_1 = (boost::lambda::bind(&foo, _1, a, 5) - 12)));

Listing 4. Przekazywanie parametrów do wyrażeń lambda

int a = 1;
int b = 2;
int c = 3;
//wyrażenie, które nie przechwytuje żadnych parametrów
[](){std::cout << "Pusto :(";}(); //błąd kompilacji, próba użycia nieprzekazanej zmiennej [](){std::cout << a;}(); //błąd kompilacji //wszystkie zmienne są przekazane przez wartość [=](){std::cout << a << b << c;}(); //c przekazane przez referencje, reszta przez wartość [=,&c](){c = a + b;}(); //a przekazane przez wartość, reszta przez referencje [&, a](){b = a; c = a;}();

Listing 5. Deklaracje listy argumentów

//wyrażenie przyjmuje 2 parametry przez wartość, określenie typu zwracanego nie jest //konieczne
[](int a, int b){return a + b;}(1, 2);
//błąd kompilacji, próba przekazania zmiennej rvalue poprzez niestałą referencje
[](int& a, int b){ return a + b}(1, 2); //błąd kompilacji
//wyrażenie przyjmuje jeden parametr jako stałą referencję, drugi przez wartość, //określenie typu zwracanego jest konieczne
[](const int& a, int b)->int{int c = 4; return a + b + c;}(1, 2);
//zwrócenie referencji do zmiennej tymczasowej
[](int a, int b)->int&{int c = 0; return c;}(1,2);

Listing 6. Zachowanie wyrażeń lambda do późniejszego użycia

boost::function fun1 = [](int a,int b)->bool{return a > b;};
auto fun2 = [](int a,int b)->bool{return a > b;};

boost::function fun3 = _1 > _2;
auto fun4 = _1 > _2;
//wywołanie przykładowych funkcji
fun1(1,2);
fun4(4,5);

Szybki start:
Boost.Lambda: Aby rozpocząć pracę z biblioteką Boost.Lambda, należy pobrać ze strony http:://www.boost.org najnowszą wersję bibliotek boost. Lambda dostarczana jest w postaci plików nagłówkowych .hpp, więc nie będzie potrzebna kompilacja biblioteki. Wystarczy za pomocą dyrektywy #include załączyć odpowiednie pliki nagłówkowe.

Lambda C++0x: Aby rozpocząć pracę z funkcjami lambda, bądź dowolnym mechanizmem nowego standardu języka C++0x, należy zaopatrzyć się w środowisko programistyczne Microsoft Visual Studio 2010, bądź kompilator GCC w wersji co najmniej 4.5 oraz w linii poleceń użyć opcji „–std=c++11”. Należy również pamiętać, że obsługa nowego standardu jest ciągle w fazie eksperymentalnej.

Gdzie używać funkcji lambda:

  • Tam gdzie istnieje potrzeba napisana prostego obiektu funkcyjnego.
  • Tam gdzie chcemy zaadoptować obiekt funkcyjny do potrzeb algorytmu.
  • Tam gdzie chcemy zmienić kolejność lub liczbę przekazywanych argumentów.

W sieci:
http://www.boost.org
http://www.cplusplus.com/
http://gcc.gnu.org/

Autor:
Marcin Bryk
Programista C++ z Nokia Siemens Networks z wieloletnim doświadczeniem

Contact Us