[ lista przechwytywania ] ( parametry ) -> typ_zwracany { ciało }
W nawiasie okrągłym podajemy listę parametrów, jak dla zwykłej
funkcji. Po „strzałce” podajemy typ zwracany funkcji. W pewnych
(a właściwie w większości) sytuacji można tę część opuścić,
a kompilator sam ten typ wydedukuje — będzie to
decltype
zwracanego wyrażenia
(o
decltype
pisaliśmy
w rozdziale o typach danych ).
Jeśli w ciele funkcji nie ma instrukcji
return,
wydedukowanym typem będzie
void.
Na początku wyrażenia mamy kwadratowe nawiasy, które mogą być puste — oznacza to wtedy, że wszystkie potrzebne dane funkcja otrzymuje poprzez argumenty wywołania. Można jednak umieścić w tych nawiasach, oddzielone przecinkami, symbole:
Typem funkcji lambda jest nieokreślony przez standard typ, zwykle inny dla każdej lambdy. Co jednak ważne, typ ten jest konwertowalny do typu function<Typ(Typy)> (z nagłówka functional), gdzie Typ jest typem zwracanym funkcji (być może void), a Typy to typy argumentów oddzielone przecinkami. Zwykłe wskaźniki funkcyjne też są konwertowane to tego rodzaju typów automatycznie. W wielu, ale nie wszystkich, przypadkach można się posłużyć słowem kluczowym auto aby uniknąć konieczności jawnego definiowania typu.
1. #include <iostream>
2. #include <functional>
3. using std::cout; using std::endl;
4.
5. double square(double x) {
6. return x*x;
7. }
8.
9. void invoke(std::function<double(double)> f, double arg) {
10. double res = f(arg);
11. cout << "invoke(" << arg << ")=" << res << endl;
12. }
13.
14. int main() {
15. // pomocnicza funkcja lambda
16. auto print =
17. [](double p1, double p2, double p3,
18. double arg, double val) -> void
19. {
20. cout << " a=" << p1 << " b=" << p2
21. << " c=" << p3 << " x=" << arg
22. << " res=" << val << endl;
23. };
24.
25. // funkcja lambda a*x*x+b*x+c
26. int a = 1, b = 1, c = 1;
27. // lokalne zmienne przez wartość
28. auto pol1 =
29. [=](double x) -> double
30. {
31. double res = c+x*(b+x*a);
32. print(a,b,c,x,res);
33. return res;
34. };
35. cout << "pol1=" << pol1(2) << endl;
36. a = b = c = 2;
37. cout << "pol1=" << pol1(2) << endl << endl;
38.
39. // lokalne zmienne przez referencje
40. auto pol2 =
41. [&](double x) -> double
42. {
43. double res = c+x*(b+x*a);
44. print(a,b,c,x,res);
45. return res;
46. };
47. cout << "pol2=" << pol2(2) << endl;
48. a = b = c = 1;
49. cout << "pol2=" << pol2(2) << endl << endl;
50.
51. // a i c przez referencje, b i print przez wartość
52. auto pol3 = ➊
53. [&a,b,&c,print](double x) -> double
54. {
55. double res = c+x*(b+x*a);
56. print(a,b,c,x,res);
57. return res;
58. };
59. cout << "pol3=" << pol3(2) << endl; ➋
60. a = b = c = 2; ➌
61. cout << "pol3=" << pol3(2) << endl << endl; ➍
62.
63. // typ określony jawnie
64. std::function<double(double)> f = pol3;
65. invoke(f,2);
66. // konwersja zwykłych wskaźników funkcyjnych
67. invoke(square,2);
68. f = square;
69. invoke(f,2);
70. // lambda w argumencie
71. invoke([](double x) {return x*x*x;}, 3);
72.
73. // dla void->void tylko nawiasy i ciało
74. [] {
75. cout << "Done" << endl;
76. }(); // zdefiniuj funkcję i od razu ją wywołaj
77. }
Funkcja invoke pobiera funkcję lambda (lub wskaźnik do funkcji odpowiedniego typu) i wywołuje przekazaną funkcję dla podanego argumentu. Na początku funkcji main (a więc wewnątrz funkcji) definiujemy pomocniczą funkcję lambda print z pustą listą przechwytywania (cała informacja będzie przekazywana poprzez argumenty). Zauważmy, że print samo jest tu zmienną lokalną. Funkcja ta jest potem wywoływana kilka razy w treści programu. Następnie używając słowa kluczowego auto definiujemy kilka prostych funkcji(pol1, pol2, pol3 — wszystkie są implementacją tego samego wielomianu drugiego stopnia ax2 + bx + c) używając różnych list przechwytywania: jedne zmienne lokalne są przekazywane przez wartość, a więc kopiowane są wartości jakie przyjmują w momencie definiowania funkcji, do innych funkcja będzie miała dostęp przez referencję, a więc będą w niej widoczne zmiany wartości odpowiednich zmiennych. Na przykład, w linii ➊ definiujemy lambdę z listą przechwytywania zawierającą aktualne wartości b (czyli 1) i print oraz referencje do a i c (które również mają wartość 1). Wywołując funkcję z x = 2 (linia ➋), otrzymujemy 7. Następnie zmieniamy wartości zmiennych a, b i c — teraz wszystkie wynoszą 2 (linia ➌). Jednak wartość b widziana przez funkcję w dalszym ciągu wynosi 1, bo zapamiętana została wartość przyjmowana w momencie definiowania lambdy. Z drugiej strony, zmienne a i c widziane są przez referencje, a więc ich zmiany będą widoczne w funkcji i wywołanie z linii ➍ da rezultat 12.
W końcowej części programu demonstrujemy konwersje zwykłych wskaźników funkcyjnych i przekazywanie funkcji lambda i wskaźników funkcyjnych do innych funkcji (w tym przypadku do funkcji invoke). Widać, że wskaźnik jest niejawnie konwertowany do typu std::function<double(double)> (konwersja w drugą stronę nie zachodzi).
Ważne jest przeanalizowanie programu i zrozumienie otrzymanego rezultatu:
a=1 b=1 c=1 x=2 res=7
pol1=7
a=1 b=1 c=1 x=2 res=7
pol1=7
a=2 b=2 c=2 x=2 res=14
pol2=14
a=1 b=1 c=1 x=2 res=7
pol2=7
a=1 b=1 c=1 x=2 res=7
pol3=7
a=2 b=1 c=2 x=2 res=12
pol3=12
a=2 b=1 c=2 x=2 res=12
invoke(2)=4
invoke(3)=27
Done
W nowszych wersjach standardu można parametry funkcji lambda
deklarować z użyciem
auto. Kompilator sam wtedy utworzy
odpowiednie wersje funkcji dedukując typy parametrów
na podstawie typów podanych argumentów. Na przykład program:
1. #include <iostream>
2.
3. int main() {
4. using namespace std::literals;
5.
6. auto pr = [] (auto e) {
7. std::cout << "Result is " << e << '\n';
8. };
9. auto f = [] (auto e1, auto e2) {
10. return e1 < e2 ? e1 : e2;
11. };
12. auto ri = f(3, 1);
13. pr(ri);
14. auto rs = f("Cindy"s , "Alice"s);
15. pr(rs);
16. }
drukuje
Result is 1
Result is Alice
Jak widzimy, jedna lambda „obsługuje” różne typy zwracane
i typy argumentów (jest to możliwe dzięki przeciążeniom metody
operator()
w klasie reprezentującej lambdę).
Zauważmy, że napisy "Alice"
i "Cindy" zostały podane z literą 's' na końcu.
Taki zapis oznacza, że napis ten ma być traktowany jako
literał klasy
string,
a nie jako C-napis — wtedy typem
tego literału byłby
const char*
(takiej składni można używać
po włączeniu przestrzeni nazw
std::literals).
Wartości przekazywane do listy przechwytywania lambdy są trakowane jako stałe. Można jednak, poprzez użycie słowa mutable za listą parametrów, dopuścić ich zmiany. Każde wywołanie może wtedy zmieniać ich wartość i wartość ta zachowuje się w obiekcie reprezentującym lambdę pomiędzy wywołaniami. Pozwala to, na przykład, budować lambdy pełniące rolę generatorów: funkcji bezparametrowych, dla których każde wywołanie dostarcza kolejnej wartości pewnej sekwencji. W programie poniżej lambda fibo (➊) będzie zwracać w kolejnych wywołaniach kolejne liczby ciągu Fibonacciego
1. #include <iomanip> // setw
2. #include <iostream>
3.
4. int main() {
5. using std::cout; using std::endl; using std::setw;
6.
7. auto fibo = [fp=-1, fn=1] () mutable { ➊
8. int d = fp; fp = fn; return fn += d;
9. };
10.
11. auto triangle = [t=0, i=0] () mutable { ➋
12. return t += i++;
13. };
14.
15. for (size_t i = 0; i <= 10; ++i)
16. cout << setw(2) << i << ":" << setw(3) << fibo()
17. << setw(3) << triangle() << endl;
18. }
Zauważmy, że wartości fp i fn (i podobnie t oraz i) nie są zmiennymi lokalnymi z otaczającego zakresu: są definiowane i inicjowane bezpośrednio na liście przechwytywania a ich typ jest automatycznie dedukowany przez kompilator na podstawie wartości inicjujących. Program drukuje
0: 0 0
1: 1 1
2: 1 3
3: 2 6
4: 3 10
5: 5 15
6: 8 21
7: 13 28
8: 21 36
9: 34 45
10: 55 55
Zauważmy, że jeśli opcja
mutable
występuje, to nawiasy okrągłe
są potrzebne nawet wtedy, gdy funkcja jest bezparametrowa. Typ
zwracany natomiast można pominąć, jeśli może on być jednoznacznie
wydedukowany przez kompilator.
T.R. Werner, 23 lutego 2022; 19:40