22 czerwca 2013

C++11 lambda

Funkcje lambda (nie posiadające nazwy) doczekały się swojej wersji w najnowszym standardzie [1]. Ich ogólna postać wygląda następująco:
[capture list] (parameter list) -> return type { function body }
  • caputre list - najczęściej pusta, zawiera listę lokalnych zmiennych
  • parameter list - można ominąć. Wygląda tak jak w przypadku zwykłych funkcji. Nie ma tu czegoś takiego jak argumenty domyślne
  • return type - można ominąć. Wygląda tak jak w przypadku zwykłych funkcji, jednak cała deklaracja lambda musi korzystać z konstrukcji "trailing return"
  • function body - wygląda tak jak w przypadku zwykłych funkcji
Lambda może używać lokalnych zmiennych pod warunkiem, że zostały zawarte w "capture list". Inne zmienne np. statyczne lub zdefiniowane poza główną funkcją, są normalnie dostępne. To jakie zmienne i w jaki sposób będą dostępne, informuje poniższa notacja.
  • [ ] - lista pusta, lambda nie może używać zmiennych z głównej funkcji
  • [name1, name2] - nazwy zmiennych oddzielone przecinkami, które będą wykorzystywane w lambdzie (domyślnie wartości są kopiowane), jeżeli jakaś nazwa zostanie poprzedzona &, lambda dostanie ją przez referencję
  • [&] - przed domniemanie (implicit), wszystkie zmienne z głównej funkcji będą dostępne przez referencję
  • [=] - przed domniemanie (implicit), wszystkie zmienne z głównej funkcji będą dostępne przez kopię. Jeżeli lambda jest wewnątrz metody to od C++20 jest to przestarzały zapis i powinien być zastąpiony przez [=, this]
  • [&, id1, id2] - zmienne id1, id2 będą dostępne przez kopię (nie poprzedzać żadnej &!!!), wszystkie inne będą domyślnie dostępne przez referencję
  • [=, &ref1, &ref2] - zmienne ref1 i ref2, będą dostępne przez referencję, wszystkie inne będą domyślnie dostępne przez kopię
  • [this], [&, this], [=, this], [&] - this (od C++17) będzie dostępne przez referencję.
  • [*this], [&, *this], [=, *this] - this (od C++17) będzie dostępne przez kopię
Pierwszy przykład wykorzystania lambdy w algorytmie std::count_if.
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
    vector<int> vec = {1, 4, 7, 2, 3};
    int evens = std::count_if(begin(vec), end(vec),
                              [](int value) { return value % 2 == 0; });
    cout << "Parzystych: " << evens << endl;

    return 0;
}
Wynik:
Parzystych: 2
Drugi przykład trochę bardziej rozbudowany. Zliczane są elementy powyżej pewnego progu (threshold), a wynik zapisywany jest do zmiennej znajdującej się w głównej funkcji.
#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> vec = {1, 4, 7, 2, 3};
    int count_above = 0;
    int threshold = 3;
    std::for_each(begin(vec), end(vec),
                  [&, threshold](int &v) { if(v > threshold) count_above++; });

    std::cout << "Values above(" << threshold << "): " << count_above << std::endl;

    return 0;
}
Wynik:
Values above(3): 2
Jeżeli chcemy zmienić wartość przechwyconej zmiennej musimy po "parameter list" dodać słowo kluczowe mutable. Ale wartość ta będzie zmieniona tylko w obrębie samej lambdy! Poniżej link, z dobrym wyjaśnieniem tego mechanizmu:
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
    vector<int> vec = {1, 2, 3, 4};
    int count = 1;
    auto change_second = [=](int v) mutable {
        if (count == 2)
            v = 222;
        count++;
        return v;
    };

    cout << "count berfore: " << count << endl;
    std::transform(begin(vec), end(vec), begin(vec), change_second);
    cout << "count after:   " << count << endl;

    cout << "New vector values: ";
    for(int& v : vec)
        cout << v << " ";

    return 0;
}
Wynik (jak się zaczyna zabawę z lambdami, trochę zaskakujący):
count berfore: 1
count after:   1
New vector values: 1 222 3 4 
Standard nie definiuje w jaki sposób funkcja lambda zostanie zaimplementowana. Ta sama lambda, w dwóch implementacjach może być dwoma zupełnie innymi typami. Do przechowywania można wykorzystać std::function, wskaźnik do funkcji lub wykorzystać detekcję tupu za pomocą auto. I właśnie korzystanie z auto jest zalecanym sposobem. std::function, może wołać lambdę, za pośrednictwem funkcji wirtualnych, co wpływa na szybkość działania programu (mechanizm type-erasure - nie weryfikowałem tego).

To czego zabrakło w C++11, a zostało naprawione w C++17, czyli obsługa this.
#include <iostream>
#include <vector>

using namespace std;

struct MyClass {
    MyClass() : vec{0, 0, 0} {
    }
    void update() {
        auto fun = [&, this]() { this->vec[1] = 9; };
        fun();
    }

    vector<int> vec;
};

int main()
{
    MyClass obj;
    obj.update();

    for (const auto& v : obj.vec)
        cout << v << " ";
}
Wynik:
0 9 0
Oczywiście sprawa się na tym nie kończy. Istnieją duża bardziej skomplikowane przypadki np. zwracanie lambdy modyfikująca pola obiektu z metody tego obiektu. Dobry opis na tym blogu [2].

Brak komentarzy:

Prześlij komentarz