Przejdź do głównej zawartości

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.

"Nienazwane funkcje, funktory, obiekty funkcyjne"

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

Składnia lamdby

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.

Lambda z listą przechwytywania
int five = 5;
auto get7 = [five] () { return five + 2; };

std::cout << get7();
Wynik (konsola)
7
zanotuj

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();
Edycja przechwyconych zmiennych

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.

Lambda z listą argumentów
auto multplyBy7 = [] (int a) { return a * 7; }; // lambda z parametrem a o typie int
std::cout << multplyBy7(5); // lambda wywołana z argumentem 5
Wynik (konsola)
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

Lambda
#include <iostream>

int main()
{
auto five = [] { return 5; };
std::cout << five();
}
Funkcja
#include <iostream>

int five()
{
return 5;
}

int main()
{
std::cout << five();
}

Lambda zwracająca kwadrat swojego argumentu

Wyrażenie lambda z argumentem
auto square = [](int x) { return x*x; };
std::cout << square(5);
Wynik (konsola)
25

Lambda wykorzystana jako reużywalny kawałek kodu w funkcji

Lambda jako funkcja 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");
Wynik (konsola)
Hello, Mark!
Welcome, Mark!
Hi, Mark!

Najczęstsze błędy

Próba użycia nieprzechwyconej zmiennej

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

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

Wersja C++

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.

std::transform

Przestrzeń nazw 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ść

Cały program
#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 << " ";
}
}
Wynik (konsola)
Przed użyciem algorytmu:
1 2 3 4 5


Po użyciu algorytmu:
1 4 9 16 25

Więcej algorytmów poznamy w lekcji drugiej.

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.

"Nienazwane funkcje, funktory, obiekty funkcyjne"

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

Składnia lamdby

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.

Lambda z listą przechwytywania
int five = 5;
auto get7 = [five] () { return five + 2; };

std::cout << get7();
Wynik (konsola)
7
zanotuj

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();
Edycja przechwyconych zmiennych

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.

Lambda z listą argumentów
auto multplyBy7 = [] (int a) { return a * 7; }; // lambda z parametrem a o typie int
std::cout << multplyBy7(5); // lambda wywołana z argumentem 5
Wynik (konsola)
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

Lambda
#include <iostream>

int main()
{
auto five = [] { return 5; };
std::cout << five();
}
Funkcja
#include <iostream>

int five()
{
return 5;
}

int main()
{
std::cout << five();
}

Lambda zwracająca kwadrat swojego argumentu

Wyrażenie lambda z argumentem
auto square = [](int x) { return x*x; };
std::cout << square(5);
Wynik (konsola)
25

Lambda wykorzystana jako reużywalny kawałek kodu w funkcji

Lambda jako funkcja 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");
Wynik (konsola)
Hello, Mark!
Welcome, Mark!
Hi, Mark!

Najczęstsze błędy

Próba użycia nieprzechwyconej zmiennej

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

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

Wersja C++

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.

std::transform

Przestrzeń nazw 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ść

Cały program
#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 << " ";
}
}
Wynik (konsola)
Przed użyciem algorytmu:
1 2 3 4 5


Po użyciu algorytmu:
1 4 9 16 25

Więcej algorytmów poznamy w lekcji drugiej.