[ 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 AliceJak 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 55Zauważ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