+48 511 790 336

szymon@artmetic.pl

Wyrażenie lambda λ w C++


Geneza

Nazwa wywodzi się od “Rachunku Lambda” stworzonego przez Alonzo Churcha w 1936r. w tym również greckiego λ oznaczającego wszystko co można wywołać przez funkcje. Co ciekawe Alonzo Church nigdy przekonująco nie wyjaśnił dlaczego obrał właśnie tą literę greckiego alfabetu, objaśniając to “eeny, meeny, minym noe lambda” co można przełożyć “Entliczek, pentliczek, czerwony stoliczek”. A sama lambda przysłużyła się do zaprzeczeniu możliwości rozwiązania problemu Hilbera.

Zastosowanie lambdy w C++

Wyrażenie lambda jest czymś bez czego można żyć i można tworzyć oprogramowanie w C++, jednak wymaga to nieco więcej tworzenia kodu i powoduje kod bardziej złożonym. Wyrażenie lambda jest używane głównie przy wykorzystaniu biblioteki standardowej STL przy wykorzystaniu algorytmów _if (std::remove_if, std::find_if, std::count_if), a także tam gdzie potrzebne jest porównanie wartości (std::sort, std::nth_element, std::lower_bound). Ogólnie można powiedzieć, że jest wykorzystywane wszędzie tam gdzie jest potrzebne jakieś kryterium w formie funkcji.

std::find_if(kontener.begin(), kontener.end(), kryterium)

ale czym dokładnie jest lambda? Lambda jest anonimową funkcją, a dlaczego anonimową, ponieważ nie posiada własnego identyfikatora. Identyfikator nie jest potrzebny, ponieważ zastosowaniem funkcji lambda z założenia jest krótkotrwały użytek np w tym samym bloku kodu.

Implementacja wyrażenia

poniżej napisałem porównanie wyrażenia lambda, zwykłego wywołania funkcji oraz obiektu funkcyjnego z przeładowanym operatorem(), co istotne w tym przykładzie i na co warto zwrócić uwagę to mniejsza ilość kodu i pomijając kwestię trudności składni wyrażenia lambda, którą objaśnię w dalszej części, to kod jest bardziej czytelny.

#include <QCoreApplication>
#include <QDebug>
#include <iostream>
#include <vector>

using namespace  std;

bool kryterium(int x)
{
    return x < 5;
}

class Funktor
{
public:
    bool operator()(int x)
    {
        return  x < 5;
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    std::vector<int>wektor{1, 2, 3, 4, 5, 6, 7, 8};
    std::vector<int>wektorFunkcja{wektor};
    std::vector<int>wektorFunktor{wektor};
    std::vector<int>wektorLambda{wektor};

    replace_if(wektorFunkcja.begin(), wektorFunkcja.end(), kryterium,60);
    replace_if(wektorFunktor.begin(), wektorFunktor.end(), Funktor(),60);
    replace_if(wektorLambda.begin(), wektorLambda.end(), [](int x) { return x < 5;},60);


    for(size_t i =0; i<wektor.size(); i++)
        qDebug() << "Wektor: " << wektor.at(i)
                 << "\t Wektor funkcja: " << wektorFunkcja.at(i)
                 << "\t Wektor funktor: " << wektorFunktor.at(i)
                 << "\t Wektor lambda: " << wektorLambda.at(i);

    return a.exec();
}
// wynik działania

Wektor:  1 	 Wektor funkcja:  60 	 Wektor funktor:  60 	 Wektor lambda:  60
Wektor:  2 	 Wektor funkcja:  60 	 Wektor funktor:  60 	 Wektor lambda:  60
Wektor:  3 	 Wektor funkcja:  60 	 Wektor funktor:  60 	 Wektor lambda:  60
Wektor:  4 	 Wektor funkcja:  60 	 Wektor funktor:  60 	 Wektor lambda:  60
Wektor:  5 	 Wektor funkcja:  5 	 Wektor funktor:  5 	 Wektor lambda:  5
Wektor:  6 	 Wektor funkcja:  6 	 Wektor funktor:  6 	 Wektor lambda:  6
Wektor:  7 	 Wektor funkcja:  7 	 Wektor funktor:  7 	 Wektor lambda:  7
Wektor:  8 	 Wektor funkcja:  8 	 Wektor funktor:  8 	 Wektor lambda:  8

Powyższa funkcje pomimo różnic w implementacji mają za zadanie zamienić wszystkie wartości wektora które są mniejsze od 5 na liczbę 60. Jak widać notacja lambdy jest najkrótsza ponieważ zapisana jest w jednej linijce i w przeciwieństwie do funktora i funkcji nie jest wywoływana poza ciałem main, co stanowi jej atut po przyswojeniu jej składni.

Zalety wyrażenia lambda

Aby zachęcić Cię do skorzystania wyrażenia lambda postanowiłem najpierw zwrócić Twoją uwagę na jej zalety, a mianowicie:

  • posiada taką samą szybkość wykonywania jak inline
  • jest prosta w zapisie
  • można ustalić stan wewnętrzny, czyli pobrać elementy które znajdują się w zakresie klasy lub zakresie lokalnym{}, w przeciwieństwie do funkcji
  • jest uniwersalne (re używalne) ponieważ inne kryterium może mieć taką samą deklaracje funkcji

Składnia wyrażenia

Składnie wyrażenia lambda można w najbardziej ogólnym skrócie przedstawić w następujący sposób:

[](){ }

czyli

[lokalne zmienne wychwytywane](argumenty){treść wyrażenia tzw. ciało}

[&](int a){return a + 7;}

patrząc na powyższy przykład to znak

[&] – oznacza że wysyłamy wszystkie zmienne lokalne jako referencje, można byłoby wysłać takie zmienne również przez wartość [=]

następnie

(int a) jest to wartość którą wysyłamy do funkcji

i ostatni element

{return a+7;} – ciało funkcji i co ono zwraca.

Co istotne i może się różnic od funkcji to skąd wiemy jaką zmienną zwraca wyrażenie lambda? Lambda ustala to automatycznie, ale w określonych okolicznościach jak poniżej może podczas kompilacji wywalić błąd:

    replace_if(wektor.begin(), wektor.end(),
               [](int x) {
        if(x > 7)
            return 5;
        return 9.0;
    },555);

return type 'double' must match previous return type 'int' when lambda expression has unspecified explicit return type

Dzieje się tak ponieważ kompilator nie może ustalić czy ma zostać zwrócona zmienna typu int czy double, aby takiej sytuacji uniknąć należy do składni dodać następujący kod:

    replace_if(wektor.begin(), wektor.end(),
               [](int x)->double {
        if(x > 7)
            return 5;
        return 9.0;
    },555);

Dzięki zmianie

[]()->typ_zwracany{}

kompilator wie jaka ma być zmienna zwracana.

Tworzenie obiektów na lambdy słowem kluczowym auto

Jeżeli nie jesteśmy w stanie ustalić co lambda zwraca to nasuwa się pytanie jak przypisać lambdę do typu wyrażenia skoro nie wiemy jaki typ jest zwracany?

Odnośnie typu zwracanego można to zrobić w następujący sposób:

 auto lambda =[]( int &x ) { std::cout << "lambda " << ++x << " !\n"; };

 int x{5}; // deklaracja w nowym stylu zamiast int x(5) lub int x = 5
 lambda(x);
 lambda(x);

W rezultacie tego otrzymamy następujące wyniki:

lambda 6 !
lambda 7 !

Nazywanie obiektów lambdy szablonem std::function

Dla bardziej dociekliwych i nie lubiących słowa auto czyli od wszystkiego do niczego, mamy możliwość notacji poniżej:

std::function <void(double)> kwadrat = [](double x){qDebug() << "kwadrat" << x *x << endl;};

 kwadrat(5);
należy pamiętać o dodaniu dołączeniu funkcji bibliotecznej #include <functional> przed przystąpieniem do implementacji. Jak nie trudno się domyśleć dzięki takiej notacji możemy również przesyłać lambdę do funkcji.

Przesyłanie wyrażenia lambda do funkcji

 void funkcjaKserokopii(int iloscKopii, std::function<void(int)> proceduraKopiowania){
 if(proceduraKopiowania == nullptr)
 {
 cout << "Niepoprawny adres procedury sądowej!" << endl;
 return;
 } 
 proceduraKopiowania(iloscKopii);
 }
 

 int main()
 { 
     funkcjaKserokopii(5, [](int odbitki){
         for(int i{0}; i < odbitki; i++)
             qDebug() << " Sporządzono" << i + 1 << " odbitkę";
     } ); 
 }  

Wybnik operacji:
  Sporządzono 1  odbitkę
  Sporządzono 2  odbitkę
  Sporządzono 3  odbitkę
  Sporządzono 4  odbitkę
  Sporządzono 5  odbitkę

Wykorzystanie zmiennych lokalnych w wyrażeniu lambda

Jeżeli chcielibyśmy skorzystać ze zmiennej lokalnej można to zrobić w następujący sposób:

int z = 90;    
replace_if(wektor.begin(), wektor.end(),
               [a](int x) {
        qDebug() << a;
        return x > 5;
    },555);

można również zadeklarować więcej zmiennych:

    int a = 90;
    int b = 30;
    replace_if(wektor.begin(), wektor.end(),
               [a,b](int x) {
        qDebug() << a << b;
        return x > 5;
    },555);

oraz przesłać wszystkie zmienne w różny sposób:

int a = 0, b = 1, c = 2;
[ & ] {}; 
// &a, &b, &c przesyłamy domyślnie wszystkie wyrażenia przez referencje
[ &, a ] {}; 
// a, &b, &c przesyłamy domyślnie wszystkie wyrażenia przez referencje a wyrażenie a przez wartość
[ = ] {};
//a, b, c przesyłamy domyślnie wszystkie zmienne przez wartość */
[ =, & b ] {}; 
// a, &b, c przesyłamy domyślnie wszystkie wyrażenia przez wartość a b przez referencje

Od wprowadzenia standardu c++14 można również definiować nowe zmienne w nawiasach [] np [int z= 9]

Lambda mutable

Lambda ma jedną bardzo ciekawe zabezpieczenie polegające na niemożności zmiany wartości kopii przesłanej zmiennej lokalnej przez wartość. To zabezpieczenie zostało wprowadzone intencjonalnie, aby programiści bezzasadnie nie zmieniali wartości kopii co nie ma większego sensu. Jednym dla opornych wymyślono rozwiązanie polegające udostępnienie takiej możliwości poprzez modyfikator „mutable”.

W definicji lambdy, po „parameter list” możemy umieścić słowo kluczowe mutable.

int a = 0;
auto lambda = [a]() mutable {return ++a;};
qDebug() << lambda(); // wywołanie lambdy

Powyższa linijka tworzy lambdę, która ma zostać preinkrementowana. Normalnie nie moglibyśmy tej operacji wykonać. Ale dzięki umieszczeniu słówka kluczowego mutable kompilator nie będzie protestował i posłusznie wykona żądaną przez programistę czynność.

Warto zwrócić uwagę na ostatnią linijkę w której lambdę wywołujemy jak funkcję. Można również zrobić to też w taki sposób:

    auto lambda = [](int c, int k, int m) {return c+k-m;};
    qDebug() << lambda(7,5,2);

wynikiem takiej operacji wówczas będzie liczba 10.

Natychmiastowe wywołanie funkcji lambda

Jeżeli użyjemy w składni [](typ_zmiennej zmienna){}(wartosc_zmiennej); to możemy natychmiast wywołać wyrażenie lambda.

    qDebug()  <<  [](int x) {return x;}(7);
    qDebug()  <<  [](int x, int y) {return x+y;}(7,16);

W kolejnym artykule postaram się przedstawić wykorzystanie lambdy w Qt. Zachęcam do pozostawienia komentarzy szczególnie tych krtycznych, które w uzasadniony i w miarę kulturalny sposób zmotywują mnie do pisania dalszych artykułów z innymi zagadnieniami.

Tagi: , , ,

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

This site uses Akismet to reduce spam. Learn how your comment data is processed.