Geneza
Nazwa wywodzi się od „Rachunku Lambda” stworzonego przez Alonzo Churcha w 1936r. w tym również greckiego λ oznaczającego wszystko co można wywołać przez funkcje. Co ciekawe Alonzo Church nigdy przekonująco nie wyjaśnił dlaczego obrał właśnie tą literę greckiego alfabetu, objaśniając to „eeny, meeny, minym noe lambda” co można przełożyć „Entliczek, pentliczek, czerwony stoliczek”. A sama lambda przysłużyła się do zaprzeczeniu możliwości rozwiązania problemu Hilbera.
Zastosowanie lambdy w C++
Wyrażenie lambda jest czymś bez czego można żyć i można tworzyć oprogramowanie w C++, jednak wymaga to nieco więcej tworzenia kodu i powoduje kod bardziej złożonym. Wyrażenie lambda jest używane głównie przy wykorzystaniu biblioteki standardowej STL przy wykorzystaniu algorytmów _if (std::remove_if, std::find_if, std::count_if), a także tam gdzie potrzebne jest porównanie wartości (std::sort, std::nth_element, std::lower_bound). Ogólnie można powiedzieć, że jest wykorzystywane wszędzie tam gdzie jest potrzebne jakieś kryterium w formie funkcji.
std::find_if(kontener.begin(), kontener.end(), kryterium)
ale czym dokładnie jest lambda? Lambda jest anonimową funkcją, a dlaczego anonimową, ponieważ nie posiada własnego identyfikatora. Identyfikator nie jest potrzebny, ponieważ zastosowaniem funkcji lambda z założenia jest krótkotrwały użytek np w tym samym bloku kodu.
Implementacja wyrażenia
poniżej napisałem porównanie wyrażenia lambda, zwykłego wywołania funkcji oraz obiektu funkcyjnego z przeładowanym operatorem(), co istotne w tym przykładzie i na co warto zwrócić uwagę to mniejsza ilość kodu i pomijając kwestię trudności składni wyrażenia lambda, którą objaśnię w dalszej części, to kod jest bardziej czytelny.
#include <QCoreApplication> #include <QDebug> #include <iostream> #include <vector> using namespace std; bool kryterium(int x) { return x < 5; } class Funktor { public: bool operator()(int x) { return x < 5; } }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); std::vector<int>wektor{1, 2, 3, 4, 5, 6, 7, 8}; std::vector<int>wektorFunkcja{wektor}; std::vector<int>wektorFunktor{wektor}; std::vector<int>wektorLambda{wektor}; replace_if(wektorFunkcja.begin(), wektorFunkcja.end(), kryterium,60); replace_if(wektorFunktor.begin(), wektorFunktor.end(), Funktor(),60); replace_if(wektorLambda.begin(), wektorLambda.end(), [](int x) { return x < 5;},60); for(size_t i =0; i<wektor.size(); i++) qDebug() << "Wektor: " << wektor.at(i) << "\t Wektor funkcja: " << wektorFunkcja.at(i) << "\t Wektor funktor: " << wektorFunktor.at(i) << "\t Wektor lambda: " << wektorLambda.at(i); return a.exec(); } // wynik działania Wektor: 1 Wektor funkcja: 60 Wektor funktor: 60 Wektor lambda: 60 Wektor: 2 Wektor funkcja: 60 Wektor funktor: 60 Wektor lambda: 60 Wektor: 3 Wektor funkcja: 60 Wektor funktor: 60 Wektor lambda: 60 Wektor: 4 Wektor funkcja: 60 Wektor funktor: 60 Wektor lambda: 60 Wektor: 5 Wektor funkcja: 5 Wektor funktor: 5 Wektor lambda: 5 Wektor: 6 Wektor funkcja: 6 Wektor funktor: 6 Wektor lambda: 6 Wektor: 7 Wektor funkcja: 7 Wektor funktor: 7 Wektor lambda: 7 Wektor: 8 Wektor funkcja: 8 Wektor funktor: 8 Wektor lambda: 8
Powyższa funkcje pomimo różnic w implementacji mają za zadanie zamienić wszystkie wartości wektora które są mniejsze od 5 na liczbę 60. Jak widać notacja lambdy jest najkrótsza ponieważ zapisana jest w jednej linijce i w przeciwieństwie do funktora i funkcji nie jest wywoływana poza ciałem main, co stanowi jej atut po przyswojeniu jej składni.
Zalety wyrażenia lambda
Aby zachęcić Cię do skorzystania wyrażenia lambda postanowiłem najpierw zwrócić Twoją uwagę na jej zalety, a mianowicie:
- posiada taką samą szybkość wykonywania jak inline
- jest prosta w zapisie
- można ustalić stan wewnętrzny, czyli pobrać elementy które znajdują się w zakresie klasy lub zakresie lokalnym{}, w przeciwieństwie do funkcji
- jest uniwersalne (re używalne) ponieważ inne kryterium może mieć taką samą deklaracje funkcji
Składnia wyrażenia
Składnie wyrażenia lambda można w najbardziej ogólnym skrócie przedstawić w następujący sposób:
[](){ }
czyli
[lokalne zmienne wychwytywane](argumenty){treść wyrażenia tzw. ciało}
[&](int a){return a + 7;}
patrząc na powyższy przykład to znak
[&] – oznacza że wysyłamy wszystkie zmienne lokalne jako referencje, można byłoby wysłać takie zmienne również przez wartość [=]
następnie
(int a) jest to wartość którą wysyłamy do funkcji
i ostatni element
{return a+7;} – ciało funkcji i co ono zwraca.
Co istotne i może się różnic od funkcji to skąd wiemy jaką zmienną zwraca wyrażenie lambda? Lambda ustala to automatycznie, ale w określonych okolicznościach jak poniżej może podczas kompilacji wywalić błąd:
replace_if(wektor.begin(), wektor.end(), [](int x) { if(x > 7) return 5; return 9.0; },555); return type 'double' must match previous return type 'int' when lambda expression has unspecified explicit return type
Dzieje się tak ponieważ kompilator nie może ustalić czy ma zostać zwrócona zmienna typu int czy double, aby takiej sytuacji uniknąć należy do składni dodać następujący kod:
replace_if(wektor.begin(), wektor.end(), [](int x)->double { if(x > 7) return 5; return 9.0; },555);
Dzięki zmianie
[]()->typ_zwracany{}
kompilator wie jaka ma być zmienna zwracana.Tworzenie obiektów na lambdy słowem kluczowym auto
Jeżeli nie jesteśmy w stanie ustalić co lambda zwraca to nasuwa się pytanie jak przypisać lambdę do typu wyrażenia skoro nie wiemy jaki typ jest zwracany?
Odnośnie typu zwracanego można to zrobić w następujący sposób:
auto lambda =[]( int &x ) { std::cout << "lambda " << ++x << " !\n"; }; int x{5}; // deklaracja w nowym stylu zamiast int x(5) lub int x = 5 lambda(x); lambda(x);
W rezultacie tego otrzymamy następujące wyniki:
lambda 6 ! lambda 7 !
Nazywanie obiektów lambdy szablonem std::function
Dla bardziej dociekliwych i nie lubiących słowa auto czyli od wszystkiego do niczego, mamy możliwość notacji poniżej:
std::function <void(double)> kwadrat = [](double x){qDebug() << "kwadrat" << x *x << endl;}; kwadrat(5);
należy pamiętać o dodaniu dołączeniu funkcji bibliotecznej #include <functional> przed przystąpieniem do implementacji. Jak nie trudno się domyśleć dzięki takiej notacji możemy również przesyłać lambdę do funkcji.
Przesyłanie wyrażenia lambda do funkcji
void funkcjaKserokopii(int iloscKopii, std::function<void(int)> proceduraKopiowania){ if(proceduraKopiowania == nullptr) { cout << "Niepoprawny adres procedury sądowej!" << endl; return; } proceduraKopiowania(iloscKopii); } int main() { funkcjaKserokopii(5, [](int odbitki){ for(int i{0}; i < odbitki; i++) qDebug() << " Sporządzono" << i + 1 << " odbitkę"; } ); } Wybnik operacji: Sporządzono 1 odbitkę Sporządzono 2 odbitkę Sporządzono 3 odbitkę Sporządzono 4 odbitkę Sporządzono 5 odbitkę
Wykorzystanie zmiennych lokalnych w wyrażeniu lambda
Jeżeli chcielibyśmy skorzystać ze zmiennej lokalnej można to zrobić w następujący sposób:
int z = 90; replace_if(wektor.begin(), wektor.end(), [a](int x) { qDebug() << a; return x > 5; },555);
można również zadeklarować więcej zmiennych:
int a = 90; int b = 30; replace_if(wektor.begin(), wektor.end(), [a,b](int x) { qDebug() << a << b; return x > 5; },555);
oraz przesłać wszystkie zmienne w różny sposób:
int a = 0, b = 1, c = 2; [ & ] {}; // &a, &b, &c przesyłamy domyślnie wszystkie wyrażenia przez referencje [ &, a ] {}; // a, &b, &c przesyłamy domyślnie wszystkie wyrażenia przez referencje a wyrażenie a przez wartość [ = ] {}; //a, b, c przesyłamy domyślnie wszystkie zmienne przez wartość */ [ =, & b ] {}; // a, &b, c przesyłamy domyślnie wszystkie wyrażenia przez wartość a b przez referencje
Od wprowadzenia standardu c++14 można również definiować nowe zmienne w nawiasach [] np [int z= 9]
Lambda mutable
Lambda ma jedną bardzo ciekawe zabezpieczenie polegające na niemożności zmiany wartości kopii przesłanej zmiennej lokalnej przez wartość. To zabezpieczenie zostało wprowadzone intencjonalnie, aby programiści bezzasadnie nie zmieniali wartości kopii co nie ma większego sensu. Jednym dla opornych wymyślono rozwiązanie polegające udostępnienie takiej możliwości poprzez modyfikator „mutable”.
W definicji lambdy, po „parameter list” możemy umieścić słowo kluczowe mutable.
int a = 0; auto lambda = [a]() mutable {return ++a;}; qDebug() << lambda(); // wywołanie lambdy |
Powyższa linijka tworzy lambdę, która ma zostać preinkrementowana. Normalnie nie moglibyśmy tej operacji wykonać. Ale dzięki umieszczeniu słówka kluczowego mutable kompilator nie będzie protestował i posłusznie wykona żądaną przez programistę czynność.
Warto zwrócić uwagę na ostatnią linijkę w której lambdę wywołujemy jak funkcję. Można również zrobić to też w taki sposób:
auto lambda = [](int c, int k, int m) {return c+k-m;}; qDebug() << lambda(7,5,2);
wynikiem takiej operacji wówczas będzie liczba 10.
Natychmiastowe wywołanie funkcji lambda
Jeżeli użyjemy w składni [](typ_zmiennej zmienna){}(wartosc_zmiennej); to możemy natychmiast wywołać wyrażenie lambda.
qDebug() << [](int x) {return x;}(7); qDebug() << [](int x, int y) {return x+y;}(7,16);
W kolejnym artykule postaram się przedstawić wykorzystanie lambdy w Qt. Zachęcam do pozostawienia komentarzy szczególnie tych krtycznych, które w uzasadniony i w miarę kulturalny sposób zmotywują mnie do pisania dalszych artykułów z innymi zagadnieniami.