Wstęp
Wszystkie rzeczy omówione poniżej wymagają C++11. Wyrażenie lambda (jak greckie λ, nie "lambada") pozwala zdefiniować anonimową funkcję, bądź obiekt funkcyjny, w miejscu użycia. Po co? Powiedzmy że chcemy sobie, na przykład spartycjonować liczby według ich reszty z dzielenia przez 5:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
bool fn( int l, int r )
{
return l % 5 < r % 5;
}
int main()
{
std::vector < int > liczby( 15 );
std::iota( liczby.begin(), liczby.end(), 0 ); std::sort( liczby.begin(), liczby.end(), fn );
for( auto it = liczby.begin(); it != liczby.end(); ++it )
std::cout << * it << ' ';
}
0 5 10 1 6 11 2 7 12 3 8 13 4 9 14
Zwróć uwagę, że żeby to osiągnąć, trzeba było zdefiniować funkcję. C++ nie pozwala tworzyć funkcji lokalnie (a przynajmniej nie zwykłych funkcji), więc funkcja pomocnicza robiąca za komparator dla sortowania musi tak niezręcznie wystawać poza kod, w którym jest potrzebna. Z pomocą przychodzą wyrażenia lambda, dzięki którym można to zapisać tak:
std::sort( liczby.begin(), liczby.end(),[ ]( int l, int r ) { return l % 5 < r % 5; } );
Wyrażenie lambda
Wyrażenie lambda w C++ składa się z 5 elementów, z czego większość jest opcjonalna. Pozwól, że przeprowadzę Cię przez nie w kolejności, w jakiej się je zapisuje w kodzie.
Kolejność jest ściśle zdefiniowana. Przykładowo:
To jest wyrażenie lambda, które niczego nie przechwytuje, nie przyjmuje żadnych argumentów i nic nie robi. Dodatkowo te wyrażenie zostało natychmiast wywołane. Nawiasy okrągłe na końcu to wywołanie, a nie lista argumentów. Lista argumentów została w tym przypadku pominięta, bo jest opcjonalna.
std::cout <<[ ]( int a, int b, int c ) { return a + b * c; }( 1, 3, 5 );
Pamiętaj, że mimo że nazywa się to 'wyrażeniem', w nawiasach klamrowych znajduje się zwykły w świecie blok kodu, więc pamiętaj o średnikach! Zwróć uwagę, że zwracamy tu liczbę typu
int, ale nie określiliśmy typu zwracanego. Definiowane zwracanego typu jest opcjonalne nawet wtedy, gdy lambda coś zwraca. Kompilator wtedy dedukuje typ zwracany na podstawie typu wyrażenia podanego w
return. Jeśli nie ma żadnego
return, lub jest podany bez wartości, to typ zwracany to
void.
std::cout <<[ ]( int a )
{
if( a < 0 )
return 0;
return a * 0.5f; }( 2 );
Ta lambda (licząca lekko zmodyfikowane ReLU) jest błędna, bo zawiera 2 różne punkty
return i w każdym typ zwracany jest inny. Powoduje to błąd kompilacji, który można rozwiązać na 2 sposoby: albo sprowadzić wszystkie returny do jednego typu, albo jawnie zdefiniować typ zwracany i wszystkie zwracane wartości zostaną przekonwertowane.
std::cout <<[ ]( int a )->float
{
if( a < 0 )
return 0;
return a * 0.5f; }( 2 );
Taki sposób zapisywania typu wartości zwracanej może być stosowany także w zwykłych funkcjach:
auto fun( int a )->int
{
return a * 2;
}
Piszemy wtedy
auto jako typ zwracany, a faktyczny typ zwracany piszemy po strzałce. Istnieją przypadki, w których typ zwracany można zapisać krócej, gdy pisze się go po nazwie funkcji, niż gdy pisze się go przed. Poza tym, czy
auto nie powinno oznaczać automatycznej dedukcji typu zwracanego, podobnie jak w wyrażeniach lambda? W C++11 jawne określenie typu zwracanego w tym zapisie wciąż jest obowiązkowe. C++14 wprowadza automatyczną dedukcję i typ po strzałce jest już opcjonalny.
Przechwytywanie nazw
Nazwy, czy raczej, poza paroma wyjątkami - zmienne automatyczne, można przechwytywać na 2 sposoby: przez referencję lub przez wartość. Zmienne przechwycone przez wartość są stałe, chyba że lambda została utworzona z atrybutem
mutable.
int a = 4, b = 0;
std::cout <<[ a, & b ] { return a + b; }();
Zmienna
a została przechwycona przez wartość, a
b przez referencję. Możemy więc modyfikować
b i nasze zmiany będą widoczne poza lambdą. Nie możemy modyfikować
a, bo jest stałe.
#include <iostream>
int main()
{
int a = 4, b = 0;
[ a, & b ]
{
++a; ++b;
}();
std::cout << a << ' ' << b << '\n';
}
Błąd możemy naprawić używając atrybutu
mutable, ale wciąż będziemy operować na lokalnej kopii zmiennej:
#include <iostream>
int main()
{
int a = 4, b = 0;
[ a, & b ]() mutable
{
++a; ++b;
}();
std::cout << a << ' ' << b << '\n';
}
4 1
Podawanie atrybutów spowoduje błąd kompilacji, jeśli nie ma podanej listy argumentów, więc w tym wypadku nawiasy okrągłe są obowiązkowe.
Alternatywną metodą przechwytywania zmiennych jest użycie zapisu
[=] (przechwyć wszystko przez wartość), oraz
[&] (przechwyć wszystko przez referencję). Jeśli naprawdę chcesz przechwycić wszystko, to te zapisy ułatwiają życie, ale jeśli chcesz przechwycić jedną, czy dwie zmienne, to lepiej wymienić je z nazwy - lepiej oddaje to Twoje intencje i kod jest czytelniejszy. Jeśli chcesz przechwycić wybrane elementy w jeden sposób i całą resztę w drugi, wciąż możesz to zrobić wymieniając te wybrane elementy z nazwy i nadając im odpowiedni sposób przechwytywania (to jest, przeciwny niż użyty do przechwycenia wszystkiego).
int a = 0, b = 1, c = 2;
[ & ] { }; [ &, a ] { }; [ = ] { }; [ =, & b ] { };
Typ wyrażenia lambda
Wyrażenie jak każde inne, więc musi mieć jakiś typ, prawda? Prawda, ale to nie jest typ, jaki da się w ogóle zapisać, bo oficjalnie typ jest nieokreślony. Dlatego najlepiej skorzystać ze zmiennej o typie dedukowanym,
auto:
auto mojaLambda =[ ]( int x ) { std::cout << "moja lambda mowi " << x << "!\n"; };
mojaLambda( 0 );
mojaLambda( 4 );
moja lambda mowi 0!
moja lambda mowi 4!
Zwróć uwagę, że nie piszemy już dodatkowych nawiasów na końcu, bo do zmiennej chcemy zapisać samą lambdę, a nie to co zwróciła po jej wywołaniu. Wróćmy teraz do słówka
mutable - jeśli zastanawiałeś się, dlaczego trzeba coś takiego jawnie podać, podczas gdy korzystając z analogii do zwykłej funkcji, argumenty przekazane przez kopię można było swobodne modyfikować bez zapowiedzi, to brawo - masz punkt. Zmienne przechwycone i zmienne przekazane przez argumenty to dwie
zupełnie różne rzeczy. Można je traktować, jakby takie nie były i to jest domyślne zachowanie, jednak
mutable to zmienia. Wróćmy do przykładu
mutable, ale wywołajmy lambdę więcej niż raz, teraz gdy wiemy, jak to zrobić:
#include <iostream>
int main()
{
int a = 4, b = 0;
auto lambda =[ a, & b ]() mutable
{
++a;
++b;
std::cout << "lambda: a: " << a << " b: " << b << '\n';
};
lambda();
lambda();
std::cout << "main: a: " << a << " b: " << b << '\n';
}
lambda: a: 5 b: 1
lambda: a: 6 b: 2
main: a: 4 b: 2
Zmienna przechwycona przez referencję zachowuje się zgodnie z oczekiwaniami - dwie inkrementacje, wszystkie widoczne na zewnątrz. Zwróć uwagę jednak na zmienną przechwyconą przez wartość. Modyfikacja tej zmiennej nie jest widoczna poza lambdą,
ale jest widoczna w drugim wywołaniu lambdy. Dzieje się tak dlatego, że lambda nie jest funkcją, lecz obiektem funkcyjnym - czymś co lepiej poznasz przy programowaniu obiektowym. Teraz wystarczy Ci wiedzieć, że obiekt funkcyjny to funkcja + stan. Dzięki temu, że lambda ma stan, w przeciwieństwie do zwykłej funkcji, może pamiętać takie modyfikacje między swoimi wywołaniami, podczas gdy są one całkowicie niewidoczne poza lambdą.
Jeśli chcemy nie tyle przechwycić istniejącą zmienną, ale utworzyć zmienną w stanie lambdy, to C++14 wprowadza taką możliwość:
auto raport =[ lp = 0 ]( const char * wiadomosc ) mutable
{
std::cout << "Wiadomosc nr " << ++lp << ": " << wiadomosc << '\n';
};
raport( "Hello world" );
raport( "Error!" );
Wiadomosc nr 1: Hello world
Wiadomosc nr 2: Error!
W C++11, trzeba by utworzyć zmienną
lp, nadać jej wartość początkową i przechwycić. A wiec zmienna nie służy nam niczym poza swoją nazwą i wartością początkową. W C++14 można oba te elementy zapisać w samym wyrażeniu lambda i nie tworzyć niepotrzebnej zmiennej.
Przekazywanie do funkcji
auto niewiele daje w przypadku definiowania argumentu funkcji, który ma przyjmować lambdę. Tu należy użyć czegoś innego, również wprowadzonego w C++11:
std::function<> z nagłówka
<functional>.
#include <iostream>
#include <functional>
void fun( std::function < int( int ) > f )
{
std::cout << "4 razy:\n";
for( int i = 0; i < 4; ++i )
std::cout << "f(" << i << ") = " << f( i ) << '\n';
}
int main()
{
fun([ ]( int x ) { return x; } );
fun([ ]( int x ) { return x * 2 + 1; } );
fun([ ]( int x ) { return x * x - 1; } );
}
4 razy:
f(0) = 0
f(1) = 1
f(2) = 2
f(3) = 3
4 razy:
f(0) = 1
f(1) = 3
f(2) = 5
f(3) = 7
4 razy:
f(0) = -1
f(1) = 0
f(2) = 3
f(3) = 8
W tym przykładzie lambdy nie przechwytują żadnych zmiennych, więc działają jak zwykłe funkcje. Takie wyrażenia lambda można przekazać również jako wskaźnik na funkcję.
Przykład
Rozwinięty program ze wstępu, używający wyrażeń lambda do stworzenia parametryzowanego predykatu sortowania
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main()
{
std::vector < int > liczby( 15 );
std::iota( liczby.begin(), liczby.end(), 0 ); for( int part = 2; part < 6; ++part )
{
std::cout << "Podzial wg wartosci % " << part << ":\n";
std::sort( liczby.begin(), liczby.end(),[ part ]( int l, int r ) { return l % part < r % part; } );
int tmp = 0;
for( auto it = liczby.begin(); it != liczby.end(); ++it )
{
if( tmp % part != * it % part )
std::cout << " | ";
else
std::cout << ' ';
tmp = * it;
std::cout << tmp;
}
std::cout << '\n';
}
}
Podzial wg wartosci % 2:
0 2 4 6 8 10 12 14 | 1 3 5 7 9 11 13
Podzial wg wartosci % 3:
0 6 12 3 9 | 4 10 1 7 13 | 2 8 14 5 11
Podzial wg wartosci % 4:
0 12 4 8 | 9 1 13 5 | 6 10 2 14 | 3 7 11
Podzial wg wartosci % 5:
0 5 10 | 1 6 11 | 12 2 7 | 8 13 3 | 4 9 14