Co to jest wyrażenie lambda w C++11?

Co to jest wyrażenie lambda w C++11? Kiedy mam go użyć? Jaki problem rozwiązują, który nie był możliwy przed ich wprowadzeniem?

Kilka przykładów i przypadków użycia byłoby przydatnych.

Author: hugomg, 2011-10-02

8 answers

Problem

C++ zawiera przydatne funkcje ogólne, takie jak std::for_each i std::transform, które mogą być bardzo przydatne. Niestety mogą być również dość uciążliwe w użyciu, szczególnie jeśli funktor , który chcesz zastosować, jest unikalny dla danej funkcji.

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Jeśli używasz f tylko raz i w tym konkretnym miejscu, to wydaje się przesadą pisanie całej klasy tylko po to, aby zrobić coś trywialnego i jednorazowego.

W C++03 możesz pokusić się o napisanie w tym celu należy wykonać następujące czynności:]}

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

Jednak nie jest to dozwolone, f nie można przekazać do szablonu funkcji w C++03.

Nowe rozwiązanie

C++11 wprowadza lambda pozwalające na napisanie inline, anonimowego functora, który zastąpi struct f. Dla małych prostych przykładów może to być czystsze do odczytania (utrzymuje wszystko w jednym miejscu) i potencjalnie prostsze do utrzymania, na przykład w najprostszym forma:

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Funkcje Lambda są tylko cukrem składniowym dla funkcji anonimowych.

Typy zwrotne

W prostych przypadkach Typ powrotu lambda jest wydedukowany dla Ciebie, np.:

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

Jednak gdy zaczniesz pisać bardziej złożone lambdy, szybko napotkasz przypadki, w których Typ zwracanego typu nie może być wydedukowany przez kompilator, np.:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Aby rozwiązać ten problem, możesz jawnie określić typ zwracanej funkcji lambda, używając -> T:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

"Przechwytywanie" zmiennych

Do tej pory nie użyliśmy niczego innego niż to, co zostało przekazane do lambda wewnątrz niego, ale możemy również użyć innych zmiennych, w obrębie lambda. Jeśli chcesz uzyskać dostęp do innych zmiennych, możesz użyć klauzuli capture ([] wyrażenia), która do tej pory nie była używana w tych przykładach, np.:

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Można przechwytywać zarówno przez odniesienie, jak i wartość, które można określić za pomocą & i = odpowiednio:

  • [&epsilon] przechwytywanie przez odniesienie
  • [&] przechwytuje wszystkie zmienne używane w lambda przez odniesienie
  • [=] przechwytuje wszystkie zmienne używane w lambda przez wartość
  • [&, epsilon] przechwytuje zmienne jak z [ & ], ale epsilon przez wartość
  • [=, &epsilon] przechwytuje zmienne jak z [=], ale epsilon przez odniesienie

Wygenerowane operator() to const domyślnie, z implikacją, że przechwytywanie będzie const, gdy uzyskasz do nich dostęp przez default. Powoduje to, że każde wywołanie z tym samym wejściem da ten sam wynik, jednak można oznaczyć lambda jako mutable zażądać, aby operator(), który jest produkowany, nie jest const.

 1239
Author: Flexo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-14 09:50:26

Co to jest funkcja lambda?

Koncepcja lambda w języku C++ wywodzi się z rachunku lambda i programowania funkcyjnego. Lambda jest funkcją bez nazwy, która jest przydatna (w rzeczywistym programowaniu, a nie teorii) dla krótkich fragmentów kodu, które nie są możliwe do ponownego użycia i nie są warte nazwania.

W C++ funkcja lambda jest zdefiniowana w następujący sposób

[]() { } // barebone lambda

Lub w całej okazałości

[]() mutable -> T { } // T is the return type, still lacking throw()

[] jest listą przechwytywania, () listą argumentów i {} ciało funkcyjne.

Lista przechwytywania

Lista przechwytywania określa, co z zewnątrz lambda powinno być dostępne wewnątrz ciała funkcji i jak. Może być albo:

  1. a wartość: [x]
  2. a reference [&x]
  3. każda zmienna aktualnie w zakresie przez odniesienie [ & ]
  4. to samo co 3, ale przez wartość [=]

Możesz wymieszać dowolne z powyższych w oddzielonej przecinkami liście [x, &y].

Lista argumentów

Lista argumentów jest taka sama jak w każdej innej funkcji C++.

Ciało funkcji

Kod, który zostanie wykonany podczas wywołania lambda.

Return type

Jeśli lambda ma tylko jedną instrukcję return, Typ return może zostać pominięty i ma typ implicit decltype(return_statement).

Mutable

Jeśli lambda jest oznaczona jako mutable (np. []() mutable { }), dozwolone jest mutowanie wartości, które zostały przechwycone przez wartość.

Przypadki użycia

The biblioteka zdefiniowana przez standard ISO w dużym stopniu korzysta z lambda i podnosi użyteczność o kilka pasków, ponieważ teraz użytkownicy nie muszą zaśmiecać kodu małymi funkcjami w pewnym dostępnym zakresie.

C++14

W C++14 lambdy zostały rozszerzone o różne propozycje.

Inicjalizacja Przechwytywania Lambda

Element listy przechwytywania można teraz zainicjować za pomocą =. Umożliwia to zmianę nazw zmiennych i przechwytywanie przez przenoszenie. Przykład zaczerpnięty z standard:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

I jeden zaczerpnięty z Wikipedii pokazujący jak przechwytywać za pomocą std::move:

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Ogólne Lambdy

Lambdy mogą być teraz ogólne (auto byłoby równoważne T tutaj, jeśli T były argumentem szablonu typu gdzieś w otaczającym zakresie):

auto lambda = [](auto x, auto y) {return x + y;};

Poprawiono Odliczenie Typu Zwrotu

C++14 pozwala wydedukować typy zwrotne dla każdej funkcji i nie ogranicza jej do funkcji postaci return expression;. Jest to również rozszerzone na lambda.

 731
Author: pmr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-03 09:26:10

Wyrażenia Lambda są zwykle używane do enkapsulacji algorytmów, aby mogły być przekazywane do innej funkcji. Jednak możliwe jest wykonanie lambda od razu po zdefiniowaniu :

[&](){ ...your code... }(); // immediately executed lambda expression

Jest funkcjonalnie równoważne

{ ...your code... } // simple code block

To sprawia, że wyrażenia lambda są potężnym narzędziem do refaktoryzacji złożonych funkcji . Zaczynasz od owinięcia sekcji kodu w funkcję lambda, jak pokazano powyżej. Proces jawnej parametryzacji można wówczas wykonać stopniowo z testami pośrednimi po każdym kroku. Po pełnym sparametryzowaniu bloku kodu (co dowodzi usunięcie &), możesz przenieść kod do lokalizacji zewnętrznej i uczynić go normalną funkcją.

Podobnie można używać wyrażeń lambda do inicjalizacji zmiennych na podstawie wyniku algorytmu ...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

Jako sposób partycjonowania logiki programu , może nawet okazać się przydatne podanie wyrażenia lambda jako argumentu do innego wyrażenia lambda...

[&]( std::function<void()> algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Wyrażenia Lambda pozwalają również tworzyć nazwane zagnieżdżone funkcje, co może być wygodnym sposobem uniknięcia powielania logiki. Używanie nazwanych lambd jest również łatwiejsze dla oczu (w porównaniu do anonimowych lambd wbudowanych), gdy przekazuje nietrywialną funkcję jako parametr do innej funkcji. uwaga: nie zapomnij średnika po zamykającym nawiasie klamrowym.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Jeśli późniejsze profilowanie ujawni znaczący narzut inicjalizacji dla obiektu function, można go przepisać jako normalną funkcję.

 153
Author: nobar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-24 09:22:53

Odpowiedzi

Q: Co to jest wyrażenie lambda w C++11?

A: pod maską jest obiektem autogenerowanej klasy z przeciążeniem operator () const . Taki obiekt nazywa się closure i jest tworzony przez kompilator. Ta koncepcja "zamknięcia" jest blisko z koncepcją bind z C++11. Ale lambdy zazwyczaj generują lepszy kod. A połączenia przez zamknięcia umożliwiają pełne inlining.

P: Kiedy mogę go użyć?

A: aby zdefiniować " proste i small logic " i zapytaj kompilatora wykonać generację z poprzedniego pytania. Podajesz kompilatorowi kilka wyrażeń, które chcesz mieć w operatorze (). Wszystkie inne rzeczy kompilator wygeneruje do ciebie.

P: jaką klasę problemów rozwiązują, które nie były możliwe przed ich wprowadzeniem?

A: jest to rodzaj cukru składniowego, jak przeciążanie operatorów zamiast funkcji dla niestandardowych operacji add, subrtact ...Ale zapisuje więcej wierszy niepotrzebnego kodu, aby zawinąć 1-3 linie prawdziwej logiki do niektórych klas itp.! Niektórzy inżynierowie uważają, że jeśli liczba linii jest mniejsza, to jest mniejsza szansa na błędy w niej (ja też tak myślę)

Przykład użycia

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Dodatki o lambdach, nie objęte pytaniem. Ignoruj tę sekcję, jeśli nie jesteś zainteresowany

1. Przechwycone wartości. Co możesz uchwycić

1.1. Możesz odwołać się do zmiennej o statycznym czasie przechowywania w lambda. Wszyscy zostali schwytani.

1.2. Możesz użyć lambda do przechwytywania wartości "według wartości". W takim przypadku przechwycone var zostaną skopiowane do obiektu function (closure).

[captureVar1,captureVar2](int arg1){}

1.3. Możesz uchwycić odniesienie. & -- w tym kontekście oznacza odniesienie, a nie wskaźniki.

   [&captureVar1,&captureVar2](int arg1){}

1.4. Istnieje notacja do przechwytywania wszystkich niestatycznych zmiennych według wartości lub przez odniesienie

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. Istnieje notacja do przechwytywania wszystkich niestatycznych var według wartości, lub przez odniesienie i określić smth. więcej. Przykłady: Przechwytywanie wszystkich zmiennych nie statycznych przez wartość, ale przez odniesienie przechwytywanie Param2

[=,&Param2](int arg1){} 

Przechwytywanie wszystkich nie-statycznych zmiennych przez odniesienie, ale przez wartość przechwytywanie Param2

[&,Param2](int arg1){} 

2. Return type

2.1. Typ powrotu Lambda można wywnioskować, jeśli lambda jest jednym wyrażeniem. Lub można go wyraźnie określić.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Jeśli lambda ma więcej niż jedno wyrażenie, to typ return musi być określony przez końcowy Typ return. Również podobna składnia może być stosowane do funkcji auto i member-functions

3. Przechwycone wartości. Czego nie można uchwycić

3.1. Można przechwytywać tylko lokalne var-y, a nie zmienną członkowską obiektu.

4. Konwersje

4.1. lambda nie jest wskaźnikiem funkcji i nie jest funkcją anonimową , ale może być niejawnie przekonwertowana na wskaźnik funkcji.

P. s.

  1. Więcej informacji na temat gramatyki lambda można znaleźć w roboczym projekcie języka programowania C++ #337, 2012-01-16, 5.1.2. Wyrażenia Lambda, p.88

  2. W C++14 dodano dodatkową funkcję o nazwie "INIT capture". Pozwala to na wykonanie arbitralnie deklaracji członków danych zamknięcia:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    
 30
Author: bruziuz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-22 21:39:58

Funkcja lambda jest funkcją anonimową, którą tworzy się w linii. Może przechwytywać zmienne, jak niektórzy wyjaśnili, (np. http://www.stroustrup.com/C++11faq. html # lambda) ale są pewne ograniczenia. Na przykład, jeśli istnieje taki interfejs wywołania zwrotnego,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

Możesz napisać funkcję na miejscu, aby użyć jej tak, jak ta przekazana do zastosowania poniżej:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Ale nie możesz tego zrobić:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Z powodu ograniczeń w standardzie C++11. Jeśli chcesz użyj captures, musisz polegać na bibliotece i

#include <functional> 

(lub jakaś inna biblioteka STL, taka jak algorytm, aby uzyskać ją pośrednio) , a następnie pracować z funkcją std::zamiast przekazywać normalne funkcje jako parametry takie jak:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}
 12
Author: Ted,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-11 00:16:13

Jedno z najlepszych wyjaśnień lambda expression jest podane od autora C++ Bjarne Stroustrup w jego książce ***The C++ Programming Language***Rozdział 11 ( ISBN-13: 978-0321563842):

What is a lambda expression?

A wyrażenie lambda , czasami nazywane również lambda funkcji lub (ściśle mówiąc niepoprawnie, ale potocznie) jako Lambda jest uproszczonym zapisem do definiowania i używania obiektu funkcji anonimowych (anonymous function object). Zamiast definiowanie nazwanej klasy za pomocą operatora (), później tworzenie obiektu tej klasy, a na koniec powołując się na to, możemy użyć skrótu.

When would I use one?

Jest to szczególnie przydatne, gdy chcemy przekazać operację jako argument do algorytmu. W kontekście graficznych interfejsów użytkownika (i gdzie indziej), takie operacje są często określane jako wywołania zwrotne.

What class of problem do they solve that wasn't possible prior to their introduction?

Tutaj chyba każda akcja wyrażenie lambda może być rozwiązane bez nich, ale z dużo większą ilością kodu i znacznie większą złożonością. Wyrażenie Lambda jest to sposób na optymalizację kodu i uczynienie go bardziej atrakcyjnym. As sad by Stroustup:

Skuteczne sposoby optymalizacji

Some examples

Poprzez wyrażenie lambda

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

Lub poprzez funkcję

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

Lub nawet

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

If u need u can name lambda expression like poniżej:

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

Lub założyć inną prostą próbkę

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

Wygeneruje następny

0

1

0

1

0

1

0

1

0

1

0 sortedx - 1;x - 3;x - 4;x - 5;x - 6;x - 7;x - 33;

[] - to jest capture list lub lambda introducer: jeśli lambdas nie wymagają dostępu do ich lokalnego środowiska możemy użyć to.

Cytat z książki:

Pierwszy znak wyrażenia lambda jest zawsze [. Lambda wprowadzenie może przybierać różne formy:

[]: pusta lista przechwytywania. To oznacza, że nie można używać nazw lokalnych z otaczającego kontekstu w ciele lambdy. Dla takich wyrażeń lambda dane są uzyskiwane z argumentów lub ze zmiennych nielokalnych.

[&]: przechwytywanie przez Referencja. Wszystkie lokalne można używać nazw. Wszystkie zmienne lokalne to dostęp przez odniesienie.

[=]: domyślnie przechwytywanie przez wartość. Wszystkie lokalne można używać nazw. Wszystkie nazwy odnoszą się do kopii zmiennych lokalnych wzięte w punkcie wywołania wyrażenia lambda.

[capture-list]: jawne przechwytywanie; capture-list jest listą nazw zmiennych lokalnych do przechwycenia (tzn. przechowywanych w obiekcie) według referencji lub wartości. Zmienne o nazwach poprzedzonych & są przechwytywane przez Referencja. Inne zmienne są rejestrowane przez wartość. Lista przechwytywania może zawierają również to i nazwy, po których następuje ... jako żywioły.

[&, capture-list] : domyślnie przechwytuje przez odwołanie się do wszystkich zmiennych lokalnych o nazwach nie męskich na liście. Lista przechwytywania może to zawierać. Wymienione nazwy nie mogą być poprzedzone przez &. Zmienne nazwane w lista przechwytywania jest rejestrowana według wartości.

[=, capture-list] : implicite capture by wartość wszystkich zmiennych lokalnych o nazwach nie wymienionych na liście. Lista przechwytywania nie może tego zawierać. Wymienione nazwy muszą być poprzedzone znakiem &. Vari-able nazwane na liście przechwytywania są przechwytywane przez odniesienie.

Zauważ, że lokalna nazwa poprzedzona & jest zawsze przechwytywana przez Referencja i nazwa lokalna, która nie jest wcześniej cedowana przez & , jest zawsze przechwytywana przez wartość. Tylko przechwytywanie przez odniesienie pozwala na modyfikację zmiennych w powołanie środowisko.

Additional

Lambda expression format

Tutaj wpisz opis obrazka

Dodatkowe referencje:

 6
Author: gbk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-09 11:11:21

Jeden problem, który rozwiązuje: Kod prostszy niż lambda dla wywołania w konstruktorze, który używa funkcji parametru wyjściowego do inicjalizacji elementu const

Możesz zainicjować const członka twojej klasy, wywołaniem funkcji, która ustawia jej wartość, zwracając jej wyjście jako parametr wyjściowy.

 1
Author: sergiol,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:29

Jednym z praktycznych zastosowań, które odkryłem, jest redukcja kodu płyty kotła. Na przykład:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

Bez lambda, może trzeba zrobić coś dla różnych bsize przypadków. Oczywiście możesz utworzyć funkcję, ale co zrobić, jeśli chcesz ograniczyć użycie w ramach funkcji soul user? charakter lambda spełnia ten wymóg i używam go do tego przypadku.

 1
Author: Misgevolution,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-30 04:24:52