11.13 Funkcje lambda

Nowy standard (C++11) wprowadza możliwość definiowania tzw. funkcji lambda. Są to anonimowe funkcje, które można definiować lokalnie. Składnia jest następująca:
       [ domknięcie ] ( 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 sytuacjach można tę część opuścić, a kompilator sam ten typ wydedukuje — tak na przykład będzie jeśli ciało funkcji składa się z jednej instrukcji return; typem zwracanym będzie wtedy decltype zwracanego wyrażenia (o  decltype pisaliśmy w rozdziale o typach danych ). 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ść.

Typem funkcji lambda jest nieokreślony przez standard typ, być może inny dla każdej lambdy, nawet jeśli takie same są typy argumentów i typ zwracany. 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. W wielu, ale nie wszystkich, przypadkach można się posłużyć słowem kluczowym auto aby uniknąć konieczności jawnego definiowania typu. Zwykłe wskaźniki funkcyjne są konwertowane to tego rodzaju typów automatycznie.

Rozpatrzmy przykład:


P80: 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.  
     71.        // dla void->void tylko nawiasy i ciało
     72.      [] {
     73.          cout << "Done" << endl;
     74.      }(); // zdefiniuj funkcję i od razu ją wywołaj
     75.  }

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 pustym domknięciem (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 domknięć: 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 domknięciem zawierającym 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)=12
    invoke(2)=4
    invoke(2)=4
    Done

T.R. Werner, 23 lutego 2019; 23:59