Funkcje lambda

Jedną z najciekawszych nowości w standardzie C++11 jest możliwość tworzenia wyrażeń lambda.

Wyrażenie lambda jest definiowane najczęściej bezpośrednio”w miejscu” jego użycia (in-place). Zwykle jest użyte jako parametr innej funkcji, oczekującej wskaźnika do funkcji lub funktora - w ogólności obiektu wywoływalnego (callable object).

Każde wyrażenie lambda powoduje utworzenie przez kompilator unikalnej klasy domknięcia (closure class), która implementuje operator wywołania funkcji posiadający implementację użytą w wyrażeniu.

Domknięciem (closure) nazywana jest instancja klasy domknięcia. W zależności od sposobu przechwycenia zmiennych lokalnych obiekt ten przechowuje kopie lub referencje do przechwyconych zmiennych.

Definiowanie wyrażeń lambda

Minimalne wyrażenie lambda:

[] {
    std::cout << "Simple lambda expression" << std::endl;
}

Lambdy mogą przyjmować parametry, a także zwracać wartość:

auto l = [] (int x, int y) {
    return x + y;
};

auto result = l(2, 3); // result == 5

Jeśli implementacja lambdy nie zawiera instrukcji return typem zwracanym lambdy jest void.

Jeśli implementacja lambdy zawiera tylko instrukcję return typem zwracanym lambdy jest typ użytego wyrażenia

W każdym innym przypadku należy zadeklarować typ zwracany:

[](bool condition) -> int {
    if (condition)
        return 1;
    else
        return 2;
}

Wygodnie jest użyć lambd do tworzenia predykatów lub funktorów wymaganych przez algorytmy standardowe (na przykład w funkcji std::sort):

std::array<double, 6> values = { 5.0, 4.0, -1.4, 7.9, -8.22, 0.4 };

// sortowanie wg wartości bezwzględnych

std::sort(values.begin(), values.end(),
          // lambda expression as comparer
          [](double a, double b) { return std::abs(a) < std::abs(b); });

Lub do zastąpienia pętli for:

// bez wyrażenia lambda
for (auto it = v.begin(); it != v.end(); ++it)
{
    cout << *it;
}

// lambda użyta z algorytmem for_each
for_each(v.begin(), v.end(), [](int val) { cout << val; });

Zakres zmiennych

Wewnątrz nawiasów kwadratowych [] możemy zawrzeć elementy, które lambda ma przechwycić z zakresu w którym jest tworzona oraz określić sposób w jaki zostaną one przechwycone.

  • [] puste nawiasy oznaczają, że wewnątrz lambdy nie można użyć jakiejkolwiek nazwy z otaczającego kontekstu

  • [&] niejawne przechwycenie przez referencję. Lambda ma dostęp do odczytu i zapisu zmiennych z zakresu w którym została utworzona. Obiekt domknięcia przechowuje referencje do zewnętrznych zmiennych.

  • [=] niejawne przechwycenie przez wartość. Mogą być użyte wszystkie nazwy z zewnętrznego kontekstu. Nazwy te odnoszą się do kopii lokalnych zmiennych zewnetrznych. Ich wartość jest taka, jaka była w momencie tworzenia lambdy (domknięcie).

    int a {5};
    
    auto add5 = [=](int x) { return x + a; };
    
  • [capture-list] jawne przechwycenie zmiennych wynienionych na liście. Domyślnie wymienione zmienne są przechwytywane przez wartość. Jeśli nazwy zmiennej jest poprzedzona przez & oznacza to przechwycenie przez referencję (np. [x, y, &z]).

    int counter {};
    
    auto inc = [&counter] { counter++; }
    
      int even_count = 0;
      int odd_count = 0;
    
      for_each(v.begin(), v.end(),
               [&even_count, &odd_count] (int n) {
                   if (n % 2 == 0)
                       ++even_count;
                   else
                       ++odd_count;
               });
    
    cout << "There are " << even_count
         << " even numbers" <<
         << " and " << odd_count <<
         << " odd numbers in the vector." << endl;
    

    Powyższy przykład można by również zrealizować przy użyciu funktora:

    class EvenCounter
    {
    public:
        explicit EvenCounter(int& even_count, int& odd_count)
            : even_count_{even_count}, odd_count_{odd_count}
        {
        }
    
        EvenCounter(const EvenCounter& source) = default;
        EvenCounter& operator=(const EvenCounter&) = delete;
    
        void operator()(int n) const
        {
            if (n % 2 == 0)
                ++even_count_;
            else
                ++odd_count_;
        }
    
    private:
        int& even_count_;
        int& odd_count_;
    };
    
    for_each(v.begin(), v.end(), EvenCounter(even_count, odd_count));
    

    Ostrzeżenie

    Jeśli zmienną łapiemy poprzez referencję, to możemy ją modyfikować. Ale oznacza to także, że jeśli funkcja lambda znajdzie się poza zasięgiem tej zmiennej (np. zostanie zwrócona z funkcji), to będziemy mieć odczynienia z „wiszącą referencją” (dangling reference).

  • [&, capture-list] niejawne przechwycenie przez referencję wszystkich zmiennych poza tymi, które są wymienione na liście (te są przechwytywane przez wartość). Lista może zawierać this.

  • [=, capture-list] niejawne przechwycenie przez wartość wszystkich zmiennych poza tymi, które są wymienione na liście i poprzedzone & są przechwytywane przez referencję. Lista nie może zawierać this.

Typ lambdy

Standard nie definiuje w jaki sposób wyrażenia lambda zostaną zaimplementowane. W praktyce każdą lambdę można sobie wyobrazić jako osobną klasę. Tym samym nawet dwie funkcje lambda, przyjmujące te same argumenty i zwracające ten sam typ - same są dwoma różnymi typami.

Jeśli chcemy przechować lambdę w zmiennej lokalnej najlepiej wykorzystać mechanizm dedukcji typu auto. Typ lambdy jest wtedy automatycznie dedukowany przez kompilator. Typ ten możemy „odczytać” w celu przekazania go jako parametru szablonu przez słowo kluczowe decltype.

auto comp = [](const unique_ptr<int>&a , const unique_ptr<int>& b) { return *a < *b; };

set<unique_ptr<int>, decltype(comp)> numbers(comp);

numbers.emplace(new int {10});
numbers.emplace(new int {6});
numbers.emplace(new int {13});
numbers.emplace(new int {2});

for(const auto& n : numbers)
    cout << *n << endl;

Innym mechanizmem przechowania lub przekazania lambdy jako parametr jest użycie std::function - nowego wrappera pozwalającego przechowywać obiekty wywoływalne (callable) - lambdy, funktory oraz wskaźniki do funkcji.

Logger logger;

queue<function<void()>> work_queue;

work_queue.push([] { cout << "Start" << endl; });
work_queue.push([&logger] { logger.log("Running"); });
work_queue.push([] { cout << "Stop" << endl; });

while(!work_queue.empty())
{
    auto work_to_do = work_queue.front();

    work_to_do();

    work_queue.pop();
}

Ostrzeżenie

Mechanizm używany przez std::function to type-erasure. W rezultacie wywołanie pośrednie funkcji lub lambdy może odbyć się za pośrednictwem funkcji wirtualnej. Zalecanym mechanizmem typowania lambd jest zatem auto.

Standard definiuje poza tym, że jeśli funkcja lambda ma pusty zakres przechwytywania, to można ją przypisać do wskaźnika do funkcji:

using FunctionPtr = int(*)();

FunctionPtr f1 = [] () -> int { return 2; };

// or more concise
FunctionPtr f2 = [] { return 2; };

cout << f1() << endl;
cout << f2() << endl;

Lambdy w C++14

Generyczne wyrażenia lambda

W C++11 parametry wyrażeń lambda musiały być zadeklarowane z użyciem konkretnego typu.

C++14 daje możliwość zadeklarowania typu parametru jako auto (generic lambda).

auto lambda = [](auto x, auto y) { return x + y; }

Powoduje to dedukcję typu parametru lambdy w ten sam sposób w jaki dedukowane są typy argumentów szablonu. W rezultacie kompilator generuje kod równoważny poniższej klasie domknięcia:

struct UnnamedClosureClass
{
    template <typename T1, typename T2>
    auto operator()(T1 x, T2 y) const
    {
        return x + y;
    }
};

auto lambda = UnnamedClosureClass();

Upraszcza to implementację wielu wyrażeń lambda:

vector<shared_ptr<Gadget>> gadgets;

//...

sort(gadgets.begin(), gadgets.end(),
     [](const auto& g1, const auto& g2) { return g1->price() < g2->price(); });

Wyrażenia przechwytujące

C++14 umożliwia zainicjowanie przechwyconej zmiennej dowolnym wyrażeniem.

Umożliwia to przechwycenie zewnętrznej zmiennej, która nie jest kopiowalna, ale jest przenaszalna (move only).

unique_ptr<Gadget> g = make_unique<Gadget>("mp3 player");

auto lambda = [gadget = move(g)] { cout << gadget->id() << endl; };

Automatyczna dedukcja zwracanego typu

W C++14 reguły dotyczące dedukcji zwracanego typu z lambdy zostały znacznie poluzowane. Automatyczna dedukcja jest realizowana również w sytuacji, gdy implementacja zawiera wiele instrukcji return o ile zwracają one dane tego samego typu.

auto it = partition(cont.begin(), cont.end(),
                    [](const auto& value) {
                        if (value % 5 == 0) return true;
                        if (value % 10 == 0) return false;
                        return false;
                    });