Wyrażenia lambda
Wyrażnia lambda, zwane częściej lambdami, to wygodny sposób na zapisanie kawałka kodu w obiekcie, który można potem przesłać do funkcji i wykorzystać. Lambdy przydają się głównie do:
- tworzenia nazwanych obiektów wewnątrz funkcji, które można potem wykorzystać jak funkcje, bez zaśmiecania globalnej przestrzeni nazw.
- tworzenia "nienazwanych" kawałków kodu, które można przesyłać do innych funkcji (np. algorytmów biblioteki standardowej).
W sekcji znajduje się kilka przykładów wykorzystania lambd. Zalecamy zajrzeć do sekcji Proste przykłady i Wykorzystanie w praktyce.
Lambdy często nazywane są nienazwanymi funkcjami (ang. anonymous function), funktorami (ang. functors)lub obiektami funkcyjnymi (ang. function object).
Żadna z tych nazw nie jest poprawna, aczkolwiek może zostać użyta w kontekście lambd. Lambdy, co prawda tworzą potajemnie obiekt, aczkolwiek same w sobie są tylko wyrażeniami.
Z uwagi na specyfikę działania wyrażeń lambda (tworzenie magicznego, niewidzialnego obiektu o nieznanym, magicznym typie), aby przypisać ich wynik do obiektu, należy użyć słowa kluczowego auto
, lub typu std::function
(o którym dowiemy się w lekcji drugiej o lambdach).
Składnia
Lambda musi posiadać ciało, w którym będzie znajdować się kod oraz listę przechwytywania (która może być pusta). Lista argumentów jest opcjonalna, aczkolwiek również często spotykana. Przy wyrażeniu lambda można jeszcze dodać atrybuty, typ zwracany i wiele innych, jednak nie są one ani obowiązkowe, ani tak często używane, więc porozmawiamy o nich w innej lekcji.
Lista przechwytywania (ang. capture list)
Jak wiemy z lekcji o funkcjach, zmienne lokalne (np. z funkcji main
) nie są znane w ciele innej funkcji.
To samo tyczy się wyrażeń lambda. Te zmienne lokalne domyślnie również nie są widoczne wewnątrz ciała lambdy, dlatego,
aby móc uzyskać do nich dostęp, należy użyć listy przechwytywania.
int five = 5;
auto get7 = [five] () { return five + 2; };
std::cout << get7();
7
W przypadku lambdy z pustą listą argumentów można pominąć okrągłe nawiasy przy deklaracji.
int five = 5;
auto get7 = [five] { return five + 2; };
std::cout << get7();
Zmiennych wychwyconych przez listę przechwytywania nie można edytować. Jest sposób na obejście tego ograniczenia, jednak porozmawiamy o tym w lekcji drugiej.
Lista parametrów
Działa tak samo jak w przypadku funkcji. Pozwala nam zadeklarować z jakimi parametrami funkcja powinna zostać wywołana, a potem przesłać do niej argumenty.
auto multplyBy7 = [] (int a) { return a * 7; }; // lambda z parametrem a o typie int
std::cout << multplyBy7(5); // lambda wywołana z argumentem 5
35
Ciało wyrażenia lambda
Jest to zwykły blok kodu. Tutaj zapisujemy instrukcje, działamy na zmiennych itp.
W ciele wyrażenia lambda może się znaleźć instrukcja return
.
Proste przykłady
Porównanie lambdy i funkcji zwracającą liczbę 5 za każdym wywołaniem
#include <iostream>
int main()
{
auto five = [] { return 5; };
std::cout << five();
}
#include <iostream>
int five()
{
return 5;
}
int main()
{
std::cout << five();
}
Lambda zwracająca kwadrat swojego argumentu
auto square = [](int x) { return x*x; };
std::cout << square(5);
25
Lambda wykorzystana jako reużywalny kawałek kodu w funkcji
void print3Hellos(std::string name) {
auto print_hello = [name](std::string hello) {
std::cout << hello << ", " << name << "!\n";
}
print_hello("Hello");
print_hello("Welcome");
print_hello("Hi");
}
// ...
print3Hellos("Mark");
Hello, Mark!
Welcome, Mark!
Hi, Mark!
Najczęstsze błędy
Próba użycia nieprzechwyconej zmiennej
int main()
{
int A = 5;
// ❌ Zmienna A nie jest znana wewnątrz addToA ❌
// auto addToA = [] (int b) { return A + b; };
// ✅ Poprawna definicja lambdy ✅
auto addToA = [A] (int b) { return A + b; };
std::cout << addToA(5) << "\n";
}
Próba zmodyfikowania przechwyconej zmiennej
int main()
{
int A = 5;
// ❌ Nie możemy tutaj zmodyfikować zmiennej A ❌
// auto addToA = [A] (int b) { A += b; };
// ✅ Póki co możemy wykorzystać zwracanie wartości.
// W dalszej części kursu dowiesz się jak modyfikować przechwycone zmienne ✅
auto addToA = [A] (int b) { return A + b; };
std::cout << addToA(5) << "\n";
}
Wykorzystanie w praktyce
Zalecamy korzystanie z najnowszej wersji C++ (poprawnie zwanej standardem) - C++20, ponieważ rozwiązania z niej są prostsze. Dla osób, które z jakiegoś powodu nie mogą zainstalować kompilatora, który wspiera najnowszy standard, zamieścimy też przykłady działające na starszej wersji.
Wykorzystnie lambdy na przykładzie algorytmu transform
Aby użyć tego algorytmu, musimy dołączyć nagłówek algorithm
.
#include <algorithm>
Co chcemy zrobić?
W naszym przykładzie stworzymy wektor oraz będziemy chcieli podnieść wszystkie jego elementy do kwadratu.
Możliwe opcje
Algorytm transform
może przyjąć zarówno funkcję, obiekt funkcyjny jak i lambdę.
Jako argument wyślemy lambdę, bo o nich jest ten rozdział. Nasza lambda przyjmnie jeden argument typu int
oraz zwróci wartość tego samego typu.
- C++20
- do C++20
ranges
W przypadku C++20 algorytm znajduje się dodatkowo w przestrzeni nazw ranges
, dlatego piszemy std::ranges::transform()
zamiast std::transform()
.
1. Źródło
Jako pierwszy argument wysyłamy wektor danych (w naszym przypadku jest to wektor int
ów).
std::vector<int> data = {1, 2, 3, 4, 5};
std::ranges::transform(data, [...]);
2. Rezultat
Jako drugi argument przesyłamy początek kontenera, do którego chcemy zapisać dane. Kontener do którego zapisujemy dane musi mieć taki sam, lub większy rozmiar od kontenera źródłowego. Możemu użyć iteratora z naszego kontenera źródłowego, lub jakiegoś innego.
std::vector<int> result;
result.resize(data.size());
std::ranges::transform(data, result.begin(), [...]);
// Też poprawnie ✅
std::ranges::transform(data, data.begin(), [...]);
3. Lambda
Najważniejsza rzecz w tym przykładzie - argument trzeci. Wysyłamy tu lambdę, która:
- Przyjmuje jeden argument tego samego typu jak kontener źródłowy (
int
w tym przypadku) - Zwraca wartość tego samego typu jak kontener docelowy (również
int
w tym przypadku)
auto square = [](int a) { return a * a; };
std::ranges::transform(data, result, square);
// Możemy również przesłać bezpośrednio, zamiast zapisaywać ją do obiektu.
std::ranges::transform(data, result.begin(), [](int a) { return a * a; });
5. Całość
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> data = {1, 2, 3, 4, 5};
std::cout << "Przed użyciem algorytmu:\n";
for(auto a : data)
{
std::cout << a << " ";
}
std::cout << "\n\n";
auto square = [](int a) { return a * a; };
std::ranges::transform(data, data.begin(), square);
std::cout << "Po użyciu algorytmu:\n";
for(auto a : data)
{
std::cout << a << " ";
}
}
Przed użyciem algorytmu:
1 2 3 4 5
Po użyciu algorytmu:
1 4 9 16 25