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