11.13 Funkcje lambda

Począwszy od wersji C++11 wprowadzono możliwość definiowania tzw. funkcji lambda. Są to anonimowe funkcje, które można definiować lokalnie. Składnia jest następująca:
       [ 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:

znak równości ('=')

funkcja będzie miała dostęp do kopii wartości wszystkich zmiennych lokalnych z bieżącego zakresu, z wyjątkiem tych wymienionych jawnie (patrz niżej);
ampersand ('&')

funkcja będzie miała dostęp do odniesień (referencji, a więc oryginałów) do wszystkich zmiennych lokalnych z bieżącego zakresu; znów — z wyjątkiem tych wymienionych jawnie;
var

gdzie var jest nazwą zmiennej lokalnej: funkcja będzie miała dostęp do kopii wartości tej zmiennej;
&var

gdzie var jest nazwą zmiennej lokalnej: funkcja będzie miała dostęp do referencji do tej zmiennej.
Na przykład, [&,a] znaczy, że w tworzonej funkcji lambda wszystkie zmienne będą widoczne poprzez referencje, a zmienna a przez jej obecną wartość. Natomiast [=,&a,&b] znaczy, że w lambdzie widoczne będą kopie wszystkich zmiennych lokalnych z wyjątkiem ab, które będą widoczne „w oryginale".

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.

Rozpatrzmy przykład:


P83: lambdas.cpp     Funkcje lambda

      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 ac (które również mają wartość 1). Wywołując funkcję z x = 2 (linia ), otrzymujemy 7. Następnie zmieniamy wartości zmiennych a, bc — 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 ac 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:


P84: lambdagen.cpp     Dedukcja typów w lambdach

      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""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

F0 = 0, F1 = 1, F2 = 1, F3 = 2,…, Fn = Fn-2 + Fn-1,…

a lambda triangle () kolejne liczby trójkątne

t0 = 0, t1 = 1, t2 = 3, t3 = 6,…, tn = tn-1 + n,…




P85: lambdamutable.cpp     Funkcje lambda z opcją mutable

      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 fpfn (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