Szukaj…


Składnia

  • [ default-capture , capture-list ] ( lista argumentów ) zmienne atrybuty specyfikacji rzutowania -> return-type { lambda-body } // Kolejność specyfikatorów i atrybutów lambda.
  • [ capture-list ] ( argument-list ) { lambda-body } // Wspólna definicja lambda.
  • [=] ( lista argumentów ) { lambda-body } // Przechwytuje wszystkie potrzebne zmienne lokalne według wartości.
  • [&] ( lista argumentów ) { lambda-body } // Przechwytuje wszystkie potrzebne zmienne lokalne przez odniesienie.
  • [ capture-list ] { lambda-body } // Lista argumentów i specyfikatory mogą zostać pominięte.

Parametry

Parametr Detale
przechwytywanie domyślne Określa sposób przechwytywania wszystkich niewymienionych zmiennych. Może być = (przechwytywanie według wartości) lub & (przechwytywanie przez odniesienie). Jeśli zostanie pominięty, zmienne niewymienione na liście są niedostępne w ciele lambda . Domyślne przechwytywanie musi poprzedzać listę przechwytywania .
lista przechwytywania Określa, w jaki sposób zmienne lokalne są dostępne w ciele lambda . Zmienne bez prefiksu są przechwytywane według wartości. Zmienne poprzedzone znakiem & są przechwytywane przez odniesienie. Wewnątrz metody klasy, this może być używany do wszystkich jej członków dostępne przez odniesienie. Zmienne niewymienione na liście są niedostępne, chyba że lista poprzedzona jest domyślnym przechwytywaniem .
lista argumentów Określa argumenty funkcji lambda.
zmienny (opcjonalnie) Zwykle zmienne przechwycone przez wartość to const . Określenie mutable powoduje, że nie są one stałe. Zmiany tych zmiennych są zachowywane między połączeniami.
specyfikacja rzutu (opcjonalnie) Określa zachowanie zgłaszania wyjątku przez funkcję lambda. Na przykład: noexcept or throw(std::exception) .
atrybuty (opcjonalnie) Dowolne atrybuty dla funkcji lambda. Na przykład, jeśli ciało lambda zawsze zgłasza wyjątek, wówczas można zastosować [[noreturn]] .
-> typ zwrotu (opcjonalnie) Określa typ zwracany przez funkcję lambda. Wymagane, gdy kompilator nie może określić typu zwrotu.
ciało lambda Blok kodu zawierający implementację funkcji lambda.

Uwagi

C ++ 17 (aktualny projekt) wprowadza lambda constexpr , w zasadzie lambda, które można oceniać w czasie kompilacji. Lambda jest automatycznie constexpr jeśli spełnia wymagania constexpr , ale możesz również określić to za pomocą słowa kluczowego constexpr :

//Explicitly define this lambdas as constexpr
[]() constexpr {
    //Do stuff
}

Co to jest wyrażenie lambda?

Wyrażenie lambda zapewnia zwięzły sposób tworzenia prostych obiektów funkcyjnych. Wyrażenie lambda jest wartością, której wynikowy obiekt nazywany jest obiektem zamknięcia , który zachowuje się jak obiekt funkcji.

Nazwa „wyrażenie lambda” pochodzi od rachunku lambda , który jest formalizmem matematycznym wymyślonym w latach 30. XX wieku przez Alonzo Churcha w celu zbadania pytań dotyczących logiki i obliczalności. Rachunek Lambda stanowił podstawę LISP , funkcjonalnego języka programowania. W porównaniu z rachunkiem lambda i LISP, wyrażenia lambda C ++ mają wspólne cechy bycia nienazwanym i do przechwytywania zmiennych z otaczającego kontekstu, ale nie mają zdolności do działania i zwracania funkcji.

Wyrażenie lambda jest często używane jako argument funkcji, które pobierają obiekt na żądanie. Może to być prostsze niż utworzenie funkcji o nazwie, która byłaby używana tylko wtedy, gdy zostanie przekazana jako argument. W takich przypadkach wyrażenia lambda są na ogół preferowane, ponieważ pozwalają na zdefiniowanie obiektów funkcji wbudowanych.

Sonda lambda składa się zazwyczaj z trzech części: listy przechwytywania [] , opcjonalnej listy parametrów () i treści {} , z których wszystkie mogą być puste:

[](){}                // An empty lambda, which does and returns nothing

Lista przechwytywania

[] to lista przechwytywania . Domyślnie lambda nie może uzyskać dostępu do zmiennych obejmującego zakres. Przechwycenie zmiennej powoduje, że jest ona dostępna w lambdzie, jako kopia lub jako odniesienie . Przechwycone zmienne stają się częścią lambda; w przeciwieństwie do argumentów funkcyjnych nie trzeba ich przekazywać podczas wywoływania lambda.

int a = 0;                       // Define an integer variable
auto f = []()   { return a*9; }; // Error: 'a' cannot be accessed
auto f = [a]()  { return a*9; }; // OK, 'a' is "captured" by value
auto f = [&a]() { return a++; }; // OK, 'a' is "captured" by reference
                                 //      Note: It is the responsibility of the programmer
                                 //      to ensure that a is not destroyed before the
                                 //      lambda is called.
auto b = f();                    // Call the lambda function. a is taken from the capture list and not passed here.

Lista parametrów

() to lista parametrów , która jest prawie taka sama jak w zwykłych funkcjach. Jeśli lambda nie przyjmuje żadnych argumentów, nawiasy te można pominąć (z wyjątkiem sytuacji, gdy trzeba zadeklarować mutable lambda). Te dwa lambda są równoważne:

auto call_foo  = [x](){ x.foo(); };
auto call_foo2 = [x]{ x.foo(); };
C ++ 14

Lista parametrów może używać typu zastępczego auto zamiast typów rzeczywistych. W ten sposób argument ten zachowuje się jak parametr szablonu szablonu funkcji. Poniższe lambdy są równoważne, gdy chcesz posortować wektor w kodzie ogólnym:

auto sort_cpp11 = [](std::vector<T>::const_reference lhs, std::vector<T>::const_reference rhs) { return lhs < rhs; }; 
auto sort_cpp14 = [](const auto &lhs, const auto &rhs) { return lhs < rhs; }; 

Ciało funkcyjne

{} to ciało , które jest takie samo jak w zwykłych funkcjach.


Wzywam lambda

Obiektem wynikowym wyrażenia lambda jest zamknięcie , które można wywołać za pomocą operator() (podobnie jak w przypadku innych obiektów funkcji):

int multiplier = 5;
auto timesFive = [multiplier](int a) { return a * multiplier; }; 
std::out << timesFive(2); // Prints 10

multiplier = 15;
std::out << timesFive(2); // Still prints 2*5 == 10

Rodzaj zwrotu

Domyślnie wywnioskowano typ zwracanego wyrażenia lambda.

[](){ return true; };

W takim przypadku typem zwracanym jest bool .

Możesz także ręcznie określić typ zwracany, używając następującej składni:

[]() -> bool { return true; };

Mutable Lambda

Obiekty przechwycone przez wartość w lambda są domyślnie niezmienne. Wynika to z tego, że operator() generowanego obiektu zamknięcia jest domyślnie const .

auto func = [c = 0](){++c; std::cout << c;};  // fails to compile because ++c
                                              // tries to mutate the state of
                                              // the lambda.

Modyfikacja może być dozwolona przy użyciu słowa kluczowego mutable , co powoduje, że operator() obiektu bliższego operator() nie jest const :

auto func = [c = 0]() mutable {++c; std::cout << c;};

Jeśli jest używane razem z typem zwracanym, przed nim występuje mutable .

auto func = [c = 0]() mutable -> int {++c; std::cout << c; return c;};

Przykład ilustrujący użyteczność lambdas

Przed C ++ 11:

C ++ 11
// Generic functor used for comparison
struct islessthan
{
    islessthan(int threshold) : _threshold(threshold) {}

    bool operator()(int value) const
    {
        return value < _threshold;
    }
private:
    int _threshold;
};

// Declare a vector
const int arr[] = { 1, 2, 3, 4, 5 };
std::vector<int> vec(arr, arr+5);

// Find a number that's less than a given input (assume this would have been function input)
int threshold = 10;
std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), islessthan(threshold));

Od C ++ 11:

C ++ 11
// Declare a vector
std::vector<int> vec{ 1, 2, 3, 4, 5 };

// Find a number that's less than a given input (assume this would have been function input)
int threshold = 10;
auto it = std::find_if(vec.begin(), vec.end(), [threshold](int value) { return value < threshold; });

Określanie typu zwrotu

W przypadku lambd z jedną instrukcją return lub wieloma instrukcjami return, których wyrażenia są tego samego typu, kompilator może wydedukować typ zwracany:

// Returns bool, because "value > 10" is a comparison which yields a Boolean result
auto l = [](int value) {
    return value > 10;
}

W przypadku lambd z wieloma instrukcjami zwrotnymi różnych typów kompilator nie może wydedukować typu zwracanego:

// error: return types must match if lambda has unspecified return type
auto l = [](int value) {
    if (value < 10) {
        return 1;
    } else {
        return 1.5;
    }
};

W takim przypadku musisz jawnie określić typ zwrotu:

// The return type is specified explicitly as 'double'
auto l = [](int value) -> double {
    if (value < 10) {
        return 1;
    } else {
        return 1.5;
    }
};

Reguły tego odpowiadają regułom auto odliczania typu. Lambdas bez jawnie określonych typów zwrotów nigdy nie zwracają referencji, więc jeśli pożądany jest typ referencji, należy również jawnie określić:

auto copy = [](X& x) { return x; };       // 'copy' returns an X, so copies its input
auto ref  = [](X& x) -> X& { return x; }; // 'ref' returns an X&, no copy

Przechwytywanie według wartości

Jeśli podasz nazwę zmiennej na liście przechwytywania, lambda przechwyci ją według wartości. Oznacza to, że wygenerowany typ zamknięcia dla lambda przechowuje kopię zmiennej. Wymaga to również, aby typ zmiennej był możliwy do skopiowania :

int a = 0;

[a]() {
    return a;   // Ok, 'a' is captured by value
};
C ++ 14
auto p = std::unique_ptr<T>(...);

[p]() {         // Compile error; `unique_ptr` is not copy-constructible
    return p->createWidget(); 
};

Od wersji C ++ 14 możliwe jest inicjowanie zmiennych na miejscu. Pozwala to na uchwycenie tylko typów ruchu w lambda.

C ++ 14
auto p = std::make_unique<T>(...);

[p = std::move(p)]() {
    return p->createWidget(); 
};

Mimo że lambda przechwytuje zmienne według wartości, gdy są one podawane według nazwy, takich zmiennych nie można domyślnie modyfikować w ciele lambda. Wynika to z faktu, że typ zamknięcia umieszcza treść lambda w deklaracji operator() const .

const ma zastosowanie do dostępu do zmiennych składowych typu zamknięcia i przechwyconych zmiennych, które są członkami zamknięcia (wszystkie wyglądają inaczej):

int a = 0;

[a]() {
    a = 2;      // Illegal, 'a' is accessed via `const`

    decltype(a) a1 = 1; 
    a1 = 2; // valid: variable 'a1' is not const
};

Aby usunąć const , musisz określić słowo kluczowe mutable na lambda:

int a = 0;

[a]() mutable {
    a = 2;      // OK, 'a' can be modified
    return a;
};

Ponieważ a zostało przechwycone przez wartość, wszelkie modyfikacje wykonane przez wywołanie lambda nie wpłyną a . Wartość została skopiowana do lambda, gdy został zbudowany tak KOPIOWAå lambda z jest oddzielony od zewnętrznej a a a zmiennej.

int a = 5 ; 
auto plus5Val = [a] (void) { return a + 5 ; } ; 
auto plus5Ref = [&a] (void) {return a + 5 ; } ; 

a = 7 ; 
std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ;
// The result will be "7, value 10, reference 12"

Ujęcie ogólne

C ++ 14

Lambdas może przechwytywać wyrażenia, a nie tylko zmienne. To pozwala lambdasowi przechowywać typy tylko ruchowe:

auto p = std::make_unique<T>(...);

auto lamb = [p = std::move(p)]() //Overrides capture-by-value of `p`.
{
  p->SomeFunc();
};

To przenosi zewnętrzną zmienną p do zmiennej przechwytywania lambda, zwanej także p . lamb posiada teraz pamięć przydzieloną przez make_unique . Ponieważ zamknięcie zawiera typ, którego nie można skopiować, oznacza to, że sam lamb nie może być kopiowany. Ale można go przenieść:

auto lamb_copy = lamb; //Illegal
auto lamb_move = std::move(lamb); //legal.

Teraz lamb_move posiada pamięć.


Zauważ, że std::function<> wymaga, aby zapisane wartości mogły być kopiowane. Możesz napisać własną std::function wymagającą tylko ruchu , lub możesz po prostu wypchnąć lambda do otoki shared_ptr :

auto shared_lambda = [](auto&& f){
  return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))]
  (auto&&...args)->decltype(auto) {
    return (*spf)(decltype(args)(args)...);
  };
};
auto lamb_shared = shared_lambda(std::move(lamb_move));

pobiera naszą lambdę tylko do przenoszenia i umieszcza swój stan we wspólnym wskaźniku, a następnie zwraca lambda, którą można skopiować, a następnie zapisać w funkcji std::function lub podobnej.


Przechwytywanie uogólnione wykorzystuje auto odliczanie typu dla typu zmiennej. Domyślnie te przechwytywania zostaną zadeklarowane jako wartości, ale mogą być również referencjami:

int a = 0;

auto lamb = [&v = a](int add) //Note that `a` and `v` have different names
{
  v += add; //Modifies `a`
};

lamb(20); //`a` becomes 20.

Przechwytywanie uogólnione wcale nie musi przechwytywać zmiennej zewnętrznej. Może przechwycić dowolne wyrażenie:

auto lamb = [p = std::make_unique<T>(...)]()
{
    p->SomeFunc();
}

Jest to przydatne do nadawania lambdom dowolnych wartości, które mogą przechowywać i potencjalnie modyfikować, bez konieczności deklarowania ich zewnętrznie lambdzie. Oczywiście przydaje się to tylko wtedy, gdy nie zamierzasz uzyskiwać dostępu do tych zmiennych po zakończeniu pracy lambda.

Przechwytywanie przez odniesienie

Jeśli nazwa zmiennej lokalnej poprzedza się znakiem & , wówczas zmienna zostanie przechwycona przez odniesienie. Koncepcyjnie oznacza to, że typ zamknięcia lambdy będzie miał zmienną odniesienia, zainicjowaną jako odniesienie do odpowiedniej zmiennej spoza zakresu lambda. Każde użycie zmiennej w ciele lambda będzie odnosić się do oryginalnej zmiennej:

// Declare variable 'a'
int a = 0;

// Declare a lambda which captures 'a' by reference
auto set = [&a]() {
    a = 1;
};

set();
assert(a == 1);

Słowo kluczowe mutable nie jest potrzebna, ponieważ sama nie jest a const .

Oczywiście przechwytywanie przez odniesienie oznacza, że lambda nie może uciec przed zakresem zmiennych, które przechwytuje. Możesz więc wywoływać funkcje, które przyjmują funkcję, ale nie możesz wywoływać funkcji, która będzie przechowywać lambda poza zakresem twoich referencji. I nie wolno ci zwracać lambda.

Domyślne przechwytywanie

Domyślnie do zmiennych lokalnych, które nie są wyraźnie określone na liście przechwytywania, nie można uzyskać dostępu z wnętrza elementu lambda. Możliwe jest jednak niejawne uchwycenie zmiennych nazwanych przez ciało lambda:

int a = 1;
int b = 2;

// Default capture by value
[=]() { return a + b; }; // OK; a and b are captured by value

// Default capture by reference
[&]() { return a + b; }; // OK; a and b are captured by reference

Jawne przechwytywanie może być nadal wykonywane obok domyślnego przechwytywania domyślnego. Jawna definicja przechwytywania zastąpi przechwytywanie domyślne:

int a = 0;
int b = 1;

[=, &b]() {
    a = 2; // Illegal; 'a' is capture by value, and lambda is not 'mutable'
    b = 2; // OK; 'b' is captured by reference
};

Ogólne lambdas

c ++ 14

Funkcje lambda mogą przyjmować argumenty dowolnych typów. Dzięki temu lambda jest bardziej ogólna:

auto twice = [](auto x){ return x+x; };

int i = twice(2); // i == 4
std::string s = twice("hello"); // s == "hellohello"

Jest to zaimplementowane w C ++, powodując, że operator() typu zamknięcia przeciąża funkcję szablonu. Następujący typ zachowuje się tak samo jak powyższe zamknięcie lambda:

struct _unique_lambda_type
{
  template<typename T>
  auto operator() (T x) const {return x + x;}
};

Nie wszystkie parametry w ogólnej lambda muszą być ogólne:

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

Tutaj x jest wywnioskowane na podstawie argumentu pierwszej funkcji, podczas gdy y zawsze będzie int .

Ogólne lambdy mogą również przyjmować argumenty przez odniesienie, przy użyciu zwykłych reguł dla auto i & . Jeśli parametr generyczny jest traktowana jako auto&& , jest to odniesienie przekierowania do przekazany w argumencie, a nie odniesienie rvalue :

auto lamb1 = [](int &&x) {return x + 5;};
auto lamb2 = [](auto &&x) {return x + 5;};
int x = 10;
lamb1(x); // Illegal; must use `std::move(x)` for `int&&` parameters.
lamb2(x); // Legal; the type of `x` is deduced as `int&`.

Funkcje lambda mogą być różnorodne i doskonale przekazywać swoje argumenty:

auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);};

lub:

auto lam = [](auto&&... args){return f(decltype(args)(args)...);};

który działa tylko „poprawnie” ze zmiennymi typu auto&& .

Silnym powodem używania ogólnych lambdów jest odwiedzanie składni.

boost::variant<int, double> value;
apply_visitor(value, [&](auto&& e){
  std::cout << e;
});

Tutaj odwiedzamy w sposób polimorficzny; ale w innych kontekstach nazwy przekazywanego typu nie są interesujące:

mutex_wrapped<std::ostream&> os = std::cout;
os.write([&](auto&& os){
  os << "hello world\n";
});

Powtarzanie typu std::ostream& is noise tutaj; to tak, jakbyś musiał wymieniać typ zmiennej za każdym razem, gdy jej używasz. Tutaj tworzymy gościa, ale nie polimorficznego; auto jest używane z tego samego powodu, dla którego możesz używać auto w pętli for(:) .

Konwersja do wskaźnika funkcji

Jeśli lista przechwytywania lambda jest pusta, wówczas lambda ma niejawną konwersję do wskaźnika funkcji, który pobiera te same argumenty i zwraca ten sam typ zwrotu:

auto sorter = [](int lhs, int rhs) -> bool {return lhs < rhs;};

using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter; // implicit conversion

Taka konwersja może być również wymuszona za pomocą jednoargumentowego operatora plus:

func_ptr sorter_func2 = +sorter; // enforce implicit conversion

Wywołanie tej funkcji wskaźnika działa dokładnie tak samo, jak wywołanie operator() na lambda. Ten wskaźnik funkcji nie jest w żaden sposób zależny od istnienia źródła zamknięcia lambda. Dlatego może przeżyć zamknięcie lambda.

Ta funkcja jest przydatna głównie do używania lambd z interfejsami API, które zajmują się wskaźnikami funkcji, a nie obiektami funkcji C ++.

C ++ 14

Konwersja do wskaźnika funkcji jest również możliwa dla ogólnych lambd z pustą listą przechwytywania. W razie potrzeby do wybrania właściwej specjalizacji zostanie zastosowana dedukcja argumentów z szablonu.

auto sorter = [](auto lhs, auto rhs) { return lhs < rhs; };
using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter;  // deduces int, int
// note however that the following is ambiguous
// func_ptr sorter_func2 = +sorter;

Klasy lambdas i złapanie tego

Wyrażenie lambda ocenione w funkcji członka klasy jest domyślnie przyjacielem tej klasy:

class Foo
{
private:
    int i;
    
public:
    Foo(int val) : i(val) {}
    
    // definition of a member function
    void Test()
    {
        auto lamb = [](Foo &foo, int val)
        {
            // modification of a private member variable
            foo.i = val;
        };
        
        // lamb is allowed to access a private member, because it is a friend of Foo
        lamb(*this, 30);
    }
};

Taki lambda jest nie tylko przyjacielem tej klasy, ma taki sam dostęp jak klasa, w której jest zadeklarowany.

Lambda może przechwycić this wskaźnik, który reprezentuje instancję obiektu, w której została wywołana funkcja zewnętrzna. Odbywa się to poprzez dodanie this do listy przechwytywania:

class Foo
{
private:
    int i;
    
public:
    Foo(int val) : i(val) {}
    
    void Test()
    {
        // capture the this pointer by value
        auto lamb = [this](int val)
        {
            i = val;
        };
        
        lamb(30);
    }
};

Kiedy this zostanie przechwycone, lambda może używać nazw członków swojej zawierającej klasy tak, jakby była w swojej zawierającej klasie. Zatem ukryte this-> stosuje się do takich członków.

Należy pamiętać, że this jest zrobione przez wartość, ale nie wartość typu. Jest on przechwytywany przez wartość this , który jest wskaźnikiem . Jako taka lambda nie jest this właścicielem . Jeśli lambda się wydłuży żywotność obiektu, który ją stworzył, lambda może stać się nieważna.

Oznacza to również, że lambda może modyfikować this bez zgłoszenia mutable . Jest to wskaźnik, który jest const , a nie wskazywany obiekt. To znaczy, chyba że funkcja zewnętrznego elementu sama była funkcją const .

Pamiętaj też, że domyślne klauzule przechwytywania, zarówno [=] i [&] , również przechwycą this pośrednio. I oboje wychwytują go według wartości wskaźnika. Rzeczywiście błędem jest określenie this na liście przechwytywania, gdy podano wartość domyślną.

C ++ 17

Lambdas może przechwycić kopię this obiektu, utworzoną podczas tworzenia lambda. Dokonuje się tego poprzez dodanie *this do listy przechwytywania:

class Foo
{
private:
    int i;
    
public:
    Foo(int val) : i(val) {}
    
    void Test()
    {
        // capture a copy of the object given by the this pointer
        auto lamb = [*this](int val) mutable
        {
            i = val;
        };
        
        lamb(30); // does not change this->i
    }
};

Przenoszenie funkcji lambda do C ++ 03 za pomocą funktorów

Funkcje lambda w C ++ to cukier składniowy, który zapewnia bardzo zwięzłą składnię do pisania funktorów . W związku z tym równoważną funkcjonalność można uzyskać w C ++ 03 (aczkolwiek znacznie bardziej gadatliwie) poprzez konwersję funkcji lambda na funktor:

// Some dummy types:
struct T1 {int dummy;};
struct T2 {int dummy;};
struct R {int dummy;};

// Code using a lambda function (requires C++11)
R use_lambda(T1 val, T2 ref) {
  // Use auto because the type of the lambda is unknown.
  auto lambda = [val, &ref](int arg1, int arg2) -> R {
    /* lambda-body */
    return R();
  };
  return lambda(12, 27);
}

// The functor class (valid C++03)
// Similar to what the compiler generates for the lambda function.
class Functor {
  // Capture list.
  T1 val;
  T2& ref;

public:
  // Constructor
  inline Functor(T1 val, T2& ref) : val(val), ref(ref) {}

  // Functor body
  R operator()(int arg1, int arg2) const {
    /* lambda-body */
    return R();
  }
};

// Equivalent to use_lambda, but uses a functor (valid C++03).
R use_functor(T1 val, T2 ref) {
  Functor functor(val, ref);
  return functor(12, 27);
}

// Make this a self-contained example.
int main() {
  T1 t1;
  T2 t2;
  use_functor(t1,t2);
  use_lambda(t1,t2);
  return 0;
}

Jeśli funkcja lambda jest mutable ustaw operator wywołania funktora na non-const, tj .:

R operator()(int arg1, int arg2) /*non-const*/ {
  /* lambda-body */
  return R();
}

Rekurencyjne lambdas

Powiedzmy, że chcemy napisać gcd() jako lambda. W funkcji jest to:

int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a%b);
}

Ale lambda nie może być rekurencyjna, nie ma sposobu, by się przywołać. Lambda nie ma nazwy i użycie this w ciele lambda odnosi się do uchwyconego this (zakładając, że lambda jest tworzona w ciele funkcji składowej, w przeciwnym razie jest to błąd). Jak więc rozwiązać ten problem?

Użyj std::function

Możemy sprawić, że lambda przechwyci odniesienie do jeszcze std::function

std::function<int(int, int)> gcd = [&](int a, int b){
    return b == 0 ? a : gcd(b, a%b);
};

To działa, ale powinno być używane oszczędnie. Jest powolny (używamy teraz kasowania typu zamiast bezpośredniego wywołania funkcji), jest delikatny (kopiowanie gcd lub zwracanie gcd ulegnie gcd , ponieważ lambda odnosi się do oryginalnego obiektu) i nie będzie działać z ogólnymi lambdami.

Korzystanie z dwóch inteligentnych wskaźników:

auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>();
*gcd_self = std::make_unique<std::function<int(int, int)>>(
  [gcd_self](int a, int b){
    return b == 0 ? a : (**gcd_self)(b, a%b);
  };
};

Dodaje to dużo pośrednich (które są narzutem), ale można je kopiować / zwracać, a wszystkie kopie współużytkują stan. Pozwala ci zwrócić lambda, a poza tym jest mniej delikatny niż powyższe rozwiązanie.

Użyj kombinatora Y.

Za pomocą krótkiej struktury narzędzia możemy rozwiązać wszystkie te problemy:

template <class F>
struct y_combinator {
    F f; // the lambda will be stored here

    // a forwarding operator():
    template <class... Args>
    decltype(auto) operator()(Args&&... args) const {
        // we pass ourselves to f, then the arguments.
        // the lambda should take the first argument as `auto&& recurse` or similar.
        return f(*this, std::forward<Args>(args)...);
    }
};
// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f) {
    return {std::forward<F>(f)};
}
// (Be aware that in C++17 we can do better than a `make_` function)

możemy zaimplementować naszą gcd jako:

auto gcd = make_y_combinator(
  [](auto&& gcd, int a, int b){
    return b == 0 ? a : gcd(b, a%b);
  }
);

y_combinator to koncepcja z rachunku lambda, która pozwala na rekurencję bez możliwości nazwania się, dopóki nie zostaniesz zdefiniowany. Właśnie taki jest problem lambdów.

Tworzysz lambdę, która jako pierwszy argument przyjmuje „rekurencję”. Kiedy chcesz się powtórzyć, przekazujesz argumenty do ponownego wystąpienia.

Następnie y_combinator zwraca obiekt funkcji, który wywołuje tę funkcję z jej argumentami, ale z odpowiednim obiektem „rekurencyjnym” (mianowicie samym y_combinator ) jako pierwszym argumentem. Przekazuje również pozostałe argumenty, które wywołujesz za pomocą y_combinator na lambda.

W skrócie:

auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {
  // write body that processes some arguments
  // when you want to recurse, call recurse(some other arguments)
});

i masz rekurencję w lambda bez poważnych ograniczeń lub znacznych kosztów ogólnych.

Używanie lambdas do rozpakowywania pakietu parametrów wbudowanych

C ++ 14

Rozpakowanie pakietu parametrów tradycyjnie wymaga napisania funkcji pomocniczej za każdym razem, gdy chcesz to zrobić.

W tym przykładzie zabawki:

template<std::size_t...Is>
void print_indexes( std::index_sequence<Is...> ) {
  using discard=int[];
  (void)discard{0,((void)(
    std::cout << Is << '\n' // here Is is a compile-time constant.
  ),0)...};
}
template<std::size_t I>
void print_indexes_upto() {
  return print_indexes( std::make_index_sequence<I>{} );
}

print_indexes_upto chce utworzyć i rozpakować zestaw parametrów indeksów. W tym celu musi wywołać funkcję pomocnika. Za każdym razem, gdy chcesz rozpakować utworzony pakiet parametrów, musisz w tym celu utworzyć niestandardową funkcję pomocnika.

Można tego uniknąć dzięki lambdas.

Możesz rozpakować paczki parametrów do zestawu wywołań lambda, takich jak to:

template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;
template<std::size_t I>
constexpr index_t<I> index{};

template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
  return [](auto&& f){
    using discard=int[];
    (void)discard{0,(void(
      f( index<Is> )
    ),0)...};
  };
}

template<std::size_t N>
auto index_over(index_t<N> = {}) {
  return index_over( std::make_index_sequence<N>{} );
}
C ++ 17

Za pomocą wyrażeń krotnie można uprościć index_over() :

template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
  return [](auto&& f){
    ((void)(f(index<Is>)), ...);
  };
}

Gdy to zrobisz, możesz użyć tego, aby zastąpić konieczność ręcznego rozpakowywania pakietów parametrów drugim przeciążeniem w innym kodzie, pozwalając rozpakowywać pakiety parametrów „inline”:

template<class Tup, class F>
void for_each_tuple_element(Tup&& tup, F&& f) {
  using T = std::remove_reference_t<Tup>;
  using std::tuple_size;
  auto from_zero_to_N = index_over< tuple_size<T>{} >();

  from_zero_to_N(
    [&](auto i){
      using std::get;
      f( get<i>( std::forward<Tup>(tup) ) );
    }
  );
}

auto i przekazane do lambda przez index_over to std::integral_constant<std::size_t, ???> . Ma to constexpr konwersji do std::size_t , które nie zależy od stanu this , dzięki czemu możemy użyć go jako stałą czasu kompilacji, na przykład gdy mijamy go do std::get<i> powyżej.

Aby wrócić do przykładu zabawki u góry, przepisz go jako:

template<std::size_t I>
void print_indexes_upto() {
  index_over(index<I>)([](auto i){
    std::cout << i << '\n'; // here i is a compile-time constant
  });
}

który jest znacznie krótszy i zachowuje logikę w kodzie, który go używa.

Przykład na żywo do zabawy.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow