Wyrażenie lambda pojawiło się po raz pierwszy w wersji C++11 i można powiedzieć, że w pewnym stopniu zrewolucjonizowało cały język. Dla programistów, którzy go jeszcze nie znają, wygląda ono dość dziwacznie i niezrozumiale. Gdy jednak zrozumieją jego działanie, będą mogli je wykorzystywać w wielu sytuacjach.
Często zdarza się, że mamy jakąś funkcję, która jest używana w kodzie tylko jeden raz. Takich funkcji może być więcej. Czy nie lepiej byłoby, gdyby nie trzeba było definiować tych funkcji gdzieś poza procedurą, w których się je używa, ale robić to od razu w miejscu, w którym są potrzebne? Takie zadanie realizują właśnie wyrażenia lambda, które są zwane również funkcjami anonimowymi lub nienazwanymi. Oczywiście wyrażenia lambda są czymś więcej niż tylko takimi anonimowymi funkcjami. Ogólnie mówiąc, definiują one pełnoprawne obiekty funkcyjne, które mogą zawierać wiele składowych. Co ciekawe, typ wyrażenia lambda nie jest w ogóle definiowany przez programistę - jest generowany przez kompilator i tylko on go zna.
Warto również pamiętać o tym, że wyrażenia lambda nie tworzą jakiejś nowej konkstrukcji języka, lecz są jedynie tzw. lukrem składniowym, czyli przyczyniają się do lepszego wyglądu kodu źródłowego. Nie ma też formalnego obowiązku ich używania. Przez kompilator są one wewnętrznie zamieniane na odpowiednio skonstruowane klasy z przeciążonym operatorem wywołania funkcji. Dokładniejszy opis tego, co dzieje się "pod spodem", postaram się zamieścić w którymś z kolejnych artykułów.
Wiele osób myśli, że wyrażenia lambda są produktem stosunkowo nowym i pojawiły się w programowaniu całkiem niedawno. Okazuje się jednak, że były one już wykorzystywane na początku lat 90. XX wieku. Być może niektórzy bardziej doświadczeni programiści pamiętają język Clipper, a dokładniej mówiąc, jego wersję 5. W tej właśnie wersji pojawił się tak zwany "blok kodu" (code block). Było to nic innego, jak po prostu wyrażenie lambda, które można było podstawiać do zmiennej i ewaluować. Oto przykład:
codeBl := {|par| par += 10 }
Parametry były przekazywane pomiędzy kreskami pionowymi
||
, po czym następowała treść bloku kodu.
Na poniższym rysunku widzimy, jak wygląda konstrukcja wyrażenia lambda:
Zauważmy, że wyrażenie lambda nie wymaga deklaracji typu wartości zwracanej. Jest on dedukowany przez kompilator, dlatego też jako typu zmiennej, do której jest podstawiane wyrażenie lambda, należy używać auto
.
Utwórzmy jedno z najprostszych wyrażeń lambda i porównajmy go z odpowiednią funkcją realizującą to samo zadanie:
int fun(const int x)
{
return x + 2;
}
int main()
{
auto lambda = [](const int x) { return x + 2; };
std::cout << "Lambda = " << lambda(5) << std::endl;
std::cout << typeid(lambda).name() << std::endl;
std::cout << "Funkcja = " << fun(5) << std::endl;
std::cout << typeid(fun(5)).name() << std::endl;
}
Oto uzyskane wyniki:
Lambda = 7
class <lambda_9d155e77cdbdab7dce9e846e67991aca>
Funkcja = 7
int
Wyrażenie lambda zawiera pustą listę przechwytywania, a w obszarze parametrów przekazujemy pojedynczy parametr typu const int x
. Jak widać, parametry przekazuje się tak samo, jak w zwykłej funkcji. Treść wyrażenia lambda jest również dokładnie taka sama, jak odpowiedniej funkcji. Różnica polega jedynie na tym, że nie deklarujemy typu zwracanego. Wyrażenie lambda podstawiamy do zmiennej (obiektu funkcyjnego) lambda
, której typ jest dedukowany przez kompilator.
Pamiętajmy, że podobnie jak w przypadku funkcji bind, również obiekt funkcyjny zawierający wyrażenie lambda nie jest wykonywany aż do momentu przekazania do niego argumentów.
Widzimy, że wyrażenie lambda zwróciło oczekiwaną wartość, tzn. 7
. Dla ciekawości wyświetlmy typ zmiennej lambda
. Jak wspomniano wcześniej, typ ten jest definiowany wewnętrznie przez kompilator i znany jedynie jemu. Do wyświetlenia typu użyjemy operatora typeid
. Jak widać, typ zmiennej lambda
jest klasą o dość dziwnej nazwie. Nie można zakładać, że po każdej kompilacji nazwa klasy będzie taka sama, więc koniecznie należy stosować typ auto
.
Jeśli wyrażenie lambda nie ma parametrów, można podczas deklaracji pominąć nawiasy okrągłe:
auto lambdaEmptyPar = [] { return "Nie mam parametrów"; };
std::cout << lambdaEmptyPar() << std::endl;
Zauważmy jednak, że podczas wywołania są one potrzebne - jeśli się ich nie poda, pojawi się błąd kompilacji. Podawanie listy przechwytywania jest zawsze konieczne - na podstawie nawiasów kwadratowych kompilator rozpoznaje początek wyrażenia lambda.
Wcześniej stwierdzono, że w deklaracji wyrażenia lambda nie trzeba podawać typu zwracanego. Można to jednak zrobić (aby wykonać jawną konwersję typu lub po prostu uczynić kod bardziej czytelnym). Dokonuje się to poprzez tzw. opóźnioną deklarację typu zwracanego. Oto przykład:
auto lambdaB = [](const int x) -> char { return x + 2; };
std::cout << "Lambda = " << lambdaB(65) << std::endl;
Oto uzyskany wynik:
Lambda = C
Wyrażenie -> char
jest opóźnioną deklaracją typu zwracanego i występuje po liście parametrów. Oznacza ono, że wyrażenie lambda zwróci wartość char
. Domyślnie kompilator przyjąłby int
jako typ zwracany. W tym przypadku nastąpi konwersja typu. Argument wejściowy 65
(odpowiednik litery A
w ASCII) zostaje zwiększony o dwa i zwrócony jako znak, przez co w wyniku otrzymujemy literę C
.
Do tej pory parametry były przekazywane przez wartość. Jeśli w wyrażeniu lambda chcielibyśmy zmodyfikować wartość zewnętrznej zmiennej dostępnej w zakresie, należałoby ją przekazać poprzez referencję:
int c{ 5 };
auto lambdaC = [](int& x) { x += 2; return x; };
std::cout << "Lambda = " << lambdaC(c) << std::endl;
std::cout << "c = " << c << std::endl;
Otrzymaliśmy następujące wyniki:
Lambda = 7
c = 7
Kod zadziałał zgodnie z oczekiwaniami - zmienna c
została w wyrażeniu lambda zwiększona o 2.
Przekazywanie pewnych zmiennych z zakresu poprzez referencję lub wartość można też dokonywać za pomocą wspomnianej na wstępie listy przechwytywania. Umieszcza się w niej te zmienne z zakresu (zasięgu), które muszą być rozpoznawane przez wyrażenie lambda. Domyślnie zmienne są przekazywane przez wartość, na przykład:
int d { 10 };
auto lambdaD = [d](int x) { return d + x; };
std::cout << "Lambda = " << lambdaD(5) << std::endl;
Oto wynik:
Lambda = 15
Wyrażenie lambda użyło parametru x
oraz zmiennej d
występującej w zakresie.
Jeśli jednak wewnątrz wyrażenia lambda chcielibyśmy zmienić wartość zmiennej d
, pojawiłby się błąd kompilatora. W takim przypadku na liście przechwytywania należy zaznaczyć, że zmienna ma być przekazana jako referencja. Uzyskuje się to poprzez umieszczenie znaku &
przez nazwą zmiennej:
int d { 10 };
auto lambdaD = [&d](int x) { d += x; return d; };
std::cout << "Lambda = " << lambdaD(5) << std::endl;
std::cout << "d = " << d << std::endl;
Oto uzyskane wyniki:
Lambda = 15
d = 15
W liście przechwytywania można jedne parametry przekazywać przez wartość, a inne przez referencję:
int d { 10 };
int e { 20 };
auto lambdaE = [&d, e](int x) { d += e; return d + x; };
std::cout << "Lambda = " << lambdaE(5) << std::endl;
std::cout << "d = " << d << ", e = " << e << std::endl;
Oto uzyskane wyniki:
Lambda = 35
d = 30, e = 20
Zmienną d
przekazaliśmy przez referencję, a e
przez wartość.
Można również przechwytywać wszystkie zmienne z zakresu, w którym zostało zdefiniowane wyrażenie lambda. Takie przechwytywanie może być przez wartość lub referencję. Oto odpowiednie przykłady:
int d{ 10 };
int e{ 20 };
auto lambdaF = [=](int x) { return d + e + x; };
std::cout << "LambdaF = " << lambdaF(5) << std::endl;
auto lambdaG = [&](int x) { d += e; e += x; return d + e + x; };
std::cout << "LambdaG = " << lambdaG(5) << std::endl
std::cout << "d = " << d << ", e = " << e << std::endl;
Oto uzyskane wyniki:
LambdaF = 35
LambdaG = 60
d = 30, e = 25
W pierwszym wyrażeniu lambda lambdaF
przechwyciliśmy wszystkie zmienne przez wartość. W tym celu w liście przechwytywania użyty został znak równości =
. W drugim wyrażeniu lambda lambdaG
przechwyciliśmy zmienne przez referencję. Aby to uzyskać, na liście przechwytywania podaliśmy znak ampersandu &
.
Można też przechwytywać niektóre zmienne przez wartość, a resztę przez referencję (lub odwrotnie). Aby to osiągnąć, należy podać na liście przechwytywania symbol odnoszący się do ogólnego sposobu przechwytywania, a po nim wymienić poszczególne zmienne, które będą przechwytywane odmiennie. Oto przykład przechwytywania wszystkich zmiennych przez wartość za wyjątkiem jednej:
int d{ 10 };
int e{ 20 };
int f{ 30 };
auto lambdaH = [=, &f](int x) { f += d; return d + e + f + x; };
std::cout << "LambdaH = " << lambdaH(5) << std::endl;
std::cout << "d = " << d << ", e = " << e << ", f = " << f << std::endl;
Oto uzyskane wyniki:
LambdaH = 75
d = 10, e = 20, f = 40
Zmienne d
i e
zostały przechwycone przez wartość, a zmienna f
przez referencję.
A oto przykład odwrotny - przechwytywanie wszystkich zmiennych przez referencję za wyjątkiem jednej, która jest przechwytywana przez wartość:
int d{ 10 };
int e{ 20 };
int f{ 30 };
auto lambdaI = [&, f](int x) { d += x; e += x; return d + e + f + x; };
std::cout << "LambdaI = " << lambdaI(5) << std::endl;
std::cout << "d = " << d << ", e = " << e << ", f = " << f << std::endl;
Oto uzyskane wyniki:
LambdaI = 75
d = 15, e = 25, f = 30
Jeśli użyjemy ogólnej klauzuli &
, nie będziemy mogli jednocześnie przekazywać pojedycznych zmiennych przez referencję. I odwrotnie, jeśli użyjemy klauzuli =
, nie będziemy mogli już przekazywać pojedycznych zmiennych przez wartość. Wyrażenia takie jak [&, &d]
czy [=, e]
są błędne.
Oddzielnym przypadkiem jest przechwytywanie pól klasy. Można byłoby się spodziewać, że wystarczy na liście przechwytywania podać nazwę pola, by zostało ono przechwycone. Niestety, to nie zadziała. Aby w wyrażeniu lambda uzyskać dostęp do pól klasy, należy na liście przechwytywania umieścić wskaźnik this
:
class cA
{
public:
void fun(void)
{
auto lambdaJ = [this](int x) { return fieldA + x; };
std::cout << "LambdaJ = " << lambdaJ(5) << std::endl;
}
private:
int fieldA{ 10 };
};
int main()
{
cA obj;
obj.fun();
}
Oto uzyskane wyniki:
LambdaJ = 15
W klasie cA
zdefiniowaliśmy metodę fun
, w której z kolei zdefiniowaliśmy wyrażenie lambda lambdaJ
. Używa ono prywatnego pola fieldA
. Aby mogło to zadziałać, musieliśmy przekazać this
w liście przechwytywania. Zauważmy, że w celu odwołania się do pola klasy nie trzeba jednak używać słowa kluczowego this
- wystarczy sama nazwa pola.
Co ciekawe, przekazanie znaku =
w liście przechwytywania (który, jak już wspomiano, pozwala na przechwycenie przez wartość wszystkich zmiennych z zakresu) również uwzględnia wskaźnik this
, tak więc poniższy kod jest poprawny:
class cA
{
public:
void fun(void)
{
auto lambdaJ = [=](int x) { return fieldA + x; };
std::cout << "LambdaJ = " << lambdaJ(5) << std::endl;
}
private:
int fieldA{ 10 };
};
Na koniec warto zaznaczyć, że wyrażenie lambda może być używane "w miejscu", tzn. nie trzeba tworzyć oddzielnego obiektu funkcyjnego. Oto przykład:
int fun(int x)
{
return x + 2;
}
int main()
{
int d{ 10 };
int e{ 20 };
std::cout << "fun = " << fun([=](int x) { return d + e + x; }(5)) << std::endl;
}
Oto uzyskane wyniki:
fun = 37
Widzimy, że jako parametr funkcji fun
zostało użyte wyrażenie lambda. Nie trzeba było deklarować oddzielnego obiektu funkcyjnego. Pamiętajmy, że w takich przypadkach należy po definicji wyrażenia lambda użyć nawiasów ()
, które oznaczają jego wywołanie i zawierają argumenty.
Od programisty zależy, czy chce używać takiego zapisu, czy też woli deklarować wyrażenia lambda jako oddzielne obiekty funkcyjne. Najważniejsze znaczenie ma w tym przypadku czytelność kodu.