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:

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:

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ć:

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;
};

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ść:

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);

Spis Treści

    Funkcje Lambda
    1. Składnia funkcji lambda
    2. Mechanizm przechwytywania zmiennych (Domknięcie)
      1. Sposoby przechwytywania:
    3. Typy funkcji lambda
    4. Klauzula mutable
    5. Przykłady praktyczne
      1. Przechwytywanie zmiennych
      2. Użycie z algorytmami STL
    6. Teoretyczne podstawy funkcji lambda
    7. Mechanizm działania lambd w C++
    8. Zaawansowane zastosowania
      1. Generatory funkcji
      2. Rekursja w lambdach
    9. Wydajność i optymalizacja
    10. Nowości w nowszych standardach C++
      1. C++14: Generowane typy zwracane
      2. C++17: Domyślne szablony zmiennych
      3. C++20: Lambdy odświeżone