Last modified: September 25, 2024
This article is written in: 🇵🇱
Funkcje Lambda
Funkcje lambda, wprowadzone w standardzie C++11, stanowią jedno z najbardziej przełomowych rozszerzeń języka, umożliwiając tworzenie funkcji anonimowych bezpośrednio w miejscu ich użycia. Pozwalają one na definiowanie funkcji w sposób zwięzły i elastyczny, co znacząco ułatwia programowanie funkcyjne w C++. W niniejszym opracowaniu przedstawimy szczegółowy opis funkcji lambda, ich składni, mechanizmów przechwytywania zmiennych oraz zastosowań w praktyce, z naciskiem na precyzję i formalizm matematyczny.
Składnia funkcji lambda
Ogólna postać funkcji lambda w C++ jest następująca:
[przechwycenie](parametry) -> typ_zwracany {
// ciało funkcji
}
Elementy składni:
- Przechwycenie (
[przechwycenie]
) określa, które zmienne z zakresu otaczającego (en. enclosing scope) są dostępne wewnątrz lambdy oraz w jaki sposób są przechwytywane. - Lista parametrów (
(parametry)
) funkcji, analogicznie jak w zwykłych funkcjach. - Specyfikator typu zwracanego (
-> typ_zwracany
) opcjonalnie określa typ zwracany przez funkcję. Jeśli jest pominięty, kompilator próbuje go wywnioskować na podstawiereturn
w ciele funkcji. - Ciało funkcji (
{ ... }
) to blok kodu wykonywany przy wywołaniu lambdy.
Przykład prostej lambdy dodającej dwie liczby:
auto suma = [](int a, int b) -> int {
return a + b;
};
Mechanizm przechwytywania zmiennych (Domknięcie)
Funkcje lambda w C++ posiadają zdolność do tworzenia domknięć (en. closures), co oznacza, że mogą przechwytywać i wykorzystywać zmienne z zakresu, w którym zostały zdefiniowane. Sposób przechwytywania zmiennych określa się w nawiasach kwadratowych []
.
Sposoby przechwytywania:
[]
— brak przechwytywania. Lambda nie ma dostępu do żadnych zmiennych spoza swojego zakresu.[=]
— przechwytywanie wszystkich dostępnych zmiennych przez wartość. Zmienne są kopiowane do wnętrza lambdy.[&]
— przechwytywanie wszystkich dostępnych zmiennych przez referencję. Lambda operuje na oryginalnych zmiennych.[this]
— przechwytywanie wskaźnikathis
, umożliwiające dostęp do członków klasy.[x, &y]
— selektywne przechwytywanie:x
przez wartość,y
przez referencję.
Uwaga: Przechwytywane zmienne są traktowane jako prywatne składowe anonimowej klasy generowanej przez kompilator dla lambdy.
Typy funkcji lambda
Każda funkcja lambda jest obiektem funkcyjnym o unikalnym typie anonimowym, generowanym przez kompilator. Aby przechowywać lambdy o nieznanym z góry typie, można użyć:
auto
— do automatycznego wywnioskowania typu.std::function
— do przechowywania lambd o określonej sygnaturze, kosztem narzutu związanego z dynamicznym wywołaniem.
Przykład użycia std::function
:
std::function<int(int, int)=""> dodaj = [](int a, int b) {
return a + b;
};
Klauzula mutable
Domyślnie lambdy przechwytujące zmienne przez wartość nie pozwalają na modyfikację tych zmiennych wewnątrz swojego ciała (są one traktowane jako const
). Aby umożliwić modyfikację przechwyconych przez wartość zmiennych, należy użyć klauzuli mutable
:
int licznik = 0;
auto inkrementuj = [licznik]() mutable {
licznik++;
return licznik;
};
W powyższym przykładzie licznik
jest lokalną kopią zmiennej przechwyconej przez wartość, którą możemy modyfikować wewnątrz lambdy.
Przykłady praktyczne
Przechwytywanie zmiennych
Rozważmy zmienne a
i b
w zewnętrznym zakresie:
int a = 5;
int b = 10;
auto suma = [=]() {
return a + b;
};
auto mnoznik = [&]() {
a *= 2;
b *= 2;
};
- W lambdzie
suma
zmiennea
ib
są przechwycone przez wartość. Modyfikacjea
ib
wewnątrz lambdy nie wpłyną na oryginalne zmienne. - W lambdzie
mnoznik
zmienne są przechwycone przez referencję. Modyfikacje wewnątrz lambdy wpływają na oryginalne zmienne.
Użycie z algorytmami STL
Funkcje lambda są szczególnie użyteczne w połączeniu z algorytmami biblioteki standardowej.
Przykład sortowania z własnym kryterium:
std::vector<int> liczby = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(liczby.begin(), liczby.end(), [](int a, int b) {
return a > b; // Sortowanie malejące
});
Przykład filtrowania elementów:
std::vector<int> liczby = {1, 2, 3, 4, 5};
auto it = std::find_if(liczby.begin(), liczby.end(), [](int n) {
return n % 2 == 0; // Szukanie pierwszej liczby parzystej
});
Teoretyczne podstawy funkcji lambda
Funkcje lambda w C++ są inspirowane rachunkiem lambda, formalnym systemem logicznym opracowanym przez Alonzo Churcha w latach 30. XX wieku. Rachunek lambda jest podstawą matematycznej teorii funkcji i stanowi fundament dla języków funkcyjnych.
W kontekście C++, funkcje lambda umożliwiają traktowanie funkcji jako obiektów pierwszej klasy, co oznacza, że mogą być przekazywane jako argumenty, zwracane z funkcji oraz przechowywane w zmiennych.
Mechanizm działania lambd w C++
Podczas kompilacji lambdy są przekształcane na obiekty funkcyjne (funktory). Kompilator generuje anonimową klasę z przeciążonym operatorem wywołania funkcyjnego operator()
. Przechwycone zmienne stają się prywatnymi składowymi tej klasy.
Przykład lambdy i jej odpowiednika jako funktor:
Lambda:
auto suma = [x](int y) {
return x + y;
};
Odpowiednik jako klasa:
class AnonimowaLambda {
private:
int x;
public:
AnonimowaLambda(int x) : x(x) {}
int operator()(int y) const {
return x + y;
}
};
AnonimowaLambda suma(x);
Zaawansowane zastosowania
Generatory funkcji
Funkcje lambda mogą być zwracane z funkcji, co pozwala na tworzenie fabryk funkcji:
auto stworz_mnoznik(int mnoznik) {
return [mnoznik](int x) {
return x * mnoznik;
};
}
auto podwajaj = stworz_mnoznik(2);
std::cout << podwajaj(5); // Wyświetli 10
Rekursja w lambdach
Ze względu na anonimowość, lambdy nie posiadają nazwy, co utrudnia implementację rekurencji. Można to obejść, używając wskaźnika na samą lambdę:
std::function<int(int)> silnia = [](int n) {
return n <= 1 ? 1 : n * silnia(n - 1);
};
Lub poprzez przekazanie samej siebie jako argumentu:
auto silnia = [](auto self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
};
std::cout << silnia(silnia, 5); // Wyświetli 120
Wydajność i optymalizacja
Funkcje lambda w C++ są zazwyczaj kompilowane do wydajnego kodu maszynowego, porównywalnego z kodem napisanym za pomocą tradycyjnych funkcji czy funktorów. Jednakże nadmierne użycie std::function
może wprowadzać narzut związany z dynamicznym wywołaniem funkcji.
Aby zapewnić maksymalną wydajność:
- Unikaj używania
std::function
, jeśli nie jest to konieczne. - Przechwytuj zmienne przez referencję, jeśli kopiowanie jest kosztowne.
- Używaj
constexpr
lambd, gdy jest to możliwe (od C++17).
Nowości w nowszych standardach C++
C++14: Generowane typy zwracane
W C++14 można pominąć specyfikator typu zwracanego nawet w przypadku złożonych wyrażeń:
auto suma = [](auto a, auto b) {
return a + b;
};
C++17: Domyślne szablony zmiennych
Od C++17 lambdy mogą mieć parametry szablonowe:
auto suma = []<typename t="">(T a, T b) {
return a + b;
};
C++20: Lambdy odświeżone
C++20 wprowadza lambdy w constexpr:
constexpr auto kwadrat = [](int x) {
return x * x;
};
static_assert(kwadrat(5) == 25);