Wyrażenia lambda w języku C++

W języku C++11 lub nowszym wyrażenie lambda — często nazywane lambda — jest wygodnym sposobem definiowania obiektu funkcji anonimowej ( zamknięcia) bezpośrednio w lokalizacji, w której jest wywoływana lub przekazywana jako argument do funkcji. Zazwyczaj lambdy są używane do hermetyzacji kilku wierszy kodu, które są przekazywane do algorytmów lub funkcji asynchronicznych. W tym artykule zdefiniowano, czym są lambdy, i porównaliśmy je z innymi technikami programowania. Opisuje swoje zalety i zawiera kilka podstawowych przykładów.

Części wyrażenia lambda

Oto prosta lambda, która jest przekazywana jako trzeci argument do std::sort() funkcji:

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

Na tej ilustracji przedstawiono części składni lambda:

Diagram that identifies the various parts of a lambda expression.

Przykład wyrażenia lambda to [=]() mutable throw() -> int { return x+y; } [=] jest klauzulą capture; znany również jako lambda-introducer w specyfikacji języka C++. Nawiasy dotyczą listy parametrów. Słowo kluczowe modyfikowalne jest opcjonalne. throw() jest opcjonalną specyfikacją wyjątku. -> int jest opcjonalnym typem powrotu końcowego. Treść lambda składa się z instrukcji wewnątrz nawiasów klamrowych lub zwraca x+y; Zostały one wyjaśnione bardziej szczegółowo na poniższej ilustracji.

  1. capture klauzula (znana również jako lambda-introducer w specyfikacji języka C++).

  2. lista parametrów Opcjonalne. (Znany również jako deklarator lambda)

  3. opcjonalna specyfikacja modyfikowalna.

  4. specyfikacja wyjątku — opcjonalna.

  5. trailing-return-type Opcjonalne.

  6. ciało lambda.

Klauzula Capture

Lambda może wprowadzać nowe zmienne w treści (w języku C++14), a także uzyskiwać dostęp do zmiennych z otaczającego zakresu lub przechwytywać je. Lambda zaczyna się od klauzuli capture. Określa, które zmienne są przechwytywane, oraz określa, czy przechwytywanie jest według wartości, czy przez odwołanie. Zmienne, które mają prefiks ampersand (&), są dostępne przez odwołanie i zmienne, do których nie ma dostępu wartość.

Pusta klauzula przechwytywania wskazuje, [ ]że treść wyrażenia lambda nie uzyskuje dostępu do żadnych zmiennych w otaczającym zakresie.

Możesz użyć trybu przechwytywania domyślnego, aby wskazać, jak przechwytywać dowolne zmienne zewnętrzne, do których odwołujesz się w treści lambda: [&] oznacza, że wszystkie zmienne, do których się odwołujesz, [=] są przechwytywane przez wartość. Możesz użyć domyślnego trybu przechwytywania, a następnie jawnie określić tryb przeciwny dla określonych zmiennych. Jeśli na przykład treść lambda uzyskuje dostęp do zmiennej total zewnętrznej przy użyciu odwołania i zmiennej factor zewnętrznej według wartości, następujące klauzule przechwytywania są równoważne:

[&total, factor]
[factor, &total]
[&, factor]
[=, &total]

Tylko zmienne wymienione w treści lambda są przechwytywane po użyciu wartości capture-default.

Jeśli klauzula przechwytywania zawiera wartość capture-default &, żaden identyfikator w przechwyceniu tej klauzuli przechwytywania nie może mieć formularza &identifier. Podobnie, jeśli klauzula przechwytywania zawiera wartość capture-default =, nie można przechwycić tej klauzuli przechwytywania w postaci =identifier. Identyfikator lub this nie może pojawić się więcej niż raz w klauzuli przechwytywania. Poniższy fragment kodu ilustruje kilka przykładów:

struct S { void f(int i); };

void S::f(int i) {
    [&, i]{};      // OK
    [&, &i]{};     // ERROR: i preceded by & when & is the default
    [=, this]{};   // ERROR: this when = is the default
    [=, *this]{ }; // OK: captures this by value. See below.
    [i, i]{};      // ERROR: i repeated
}

Przechwytywanie, po którym następuje wielokropek, to rozszerzenie pakietu, jak pokazano w tym przykładzie szablonu wariadowego:

template<class... Args>
void f(Args... args) {
    auto x = [args...] { return g(args...); };
    x();
}

Aby użyć wyrażeń lambda w treści funkcji składowej klasy, przekaż this wskaźnik do klauzuli przechwytywania, aby zapewnić dostęp do funkcji składowych i składowych danych otaczającej klasy.

Program Visual Studio 2017 w wersji 15.3 lub nowszej (dostępny w /std:c++17 trybie i nowszych): wskaźnik this może zostać przechwycony przez wartość, określając *this w klauzuli przechwytywania. Przechwyć według wartości kopiuje całe zamknięcie do każdej lokacji wywołania, w której wywoływana jest lambda. (Zamknięcie to anonimowy obiekt funkcji, który hermetyzuje wyrażenie lambda). Przechwytywanie według wartości jest przydatne, gdy lambda wykonuje równolegle lub asynchroniczne operacje. Jest to szczególnie przydatne w przypadku niektórych architektur sprzętowych, takich jak NUMA.

Przykład pokazujący, jak używać wyrażeń lambda z funkcjami składowymi klasy, zobacz "Przykład: używanie wyrażenia lambda w metodzie" w temacie Przykłady wyrażeń lambda.

Jeśli używasz klauzuli capture, zalecamy, aby pamiętać o tych punktach, szczególnie w przypadku używania wyrażeń lambd z wielowątkiem:

  • Przechwytywanie odwołań może służyć do modyfikowania zmiennych poza, ale przechwytywanie wartości nie może. (mutable umożliwia modyfikowanie kopii, ale nie oryginałów).

  • Odwołania odzwierciedlają aktualizacje zmiennych poza, ale wartości nie są przechwytywane.

  • Odwołania obejmują zależność okresu istnienia, ale przechwytywanie wartości nie ma zależności okresu istnienia. Jest to szczególnie ważne, gdy lambda działa asynchronicznie. Jeśli przechwycisz lokalną wartość przy użyciu odwołania w asynchronicznych wyrażenia lambda, to lokalne może z łatwością zniknąć po uruchomieniu lambda. Kod może spowodować naruszenie dostępu w czasie wykonywania.

Uogólnione przechwytywanie (C++14)

W języku C++14 można wprowadzać i inicjować nowe zmienne w klauzuli capture bez konieczności istnienia tych zmiennych w zakresie otaczającym funkcji lambda. Inicjalizacja może być wyrażona jako dowolne wyrażenie; typ nowej zmiennej jest wywoływany z typu wygenerowanego przez wyrażenie. Ta funkcja umożliwia przechwytywanie zmiennych tylko do przenoszenia (takich jak std::unique_ptr) z otaczającego zakresu i używanie ich w zmiennej lambda.

pNums = make_unique<vector<int>>(nums);
//...
      auto a = [ptr = move(pNums)]()
        {
           // use ptr
        };

Lista parametrów

Lambdas może przechwytywać zmienne i akceptować parametry wejściowe. Lista parametrów (deklarator lambda w składni standardowej) jest opcjonalna i w większości aspektów przypomina listę parametrów funkcji.

auto y = [] (int first, int second)
{
    return first + second;
};

W języku C++14, jeśli typ parametru jest ogólny, możesz użyć auto słowa kluczowego jako specyfikatora typu. To słowo kluczowe nakazuje kompilatorowi utworzenie operatora wywołania funkcji jako szablonu. Każde wystąpienie na liście parametrów auto jest równoważne odrębnemu parametrowi typu.

auto y = [] (auto first, auto second)
{
    return first + second;
};

Wyrażenie lambda może przyjmować inne wyrażenie lambda jako argument. Aby uzyskać więcej informacji, zobacz "Wyrażenia lambda o wyższym porządku" w artykule Przykłady wyrażeń lambda.

Ponieważ lista parametrów jest opcjonalna, można pominąć puste nawiasy, jeśli argumenty nie są przekazywane do wyrażenia lambda, a jego deklarator lambda nie zawiera specyfikacji wyjątków, końcowego typu zwracanego lub mutable.

Specyfikacja modyfikowalna

Zazwyczaj operator wywołania funkcji lambda jest const-by-value, ale użycie słowa kluczowego mutable anuluje to. Nie generuje modyfikowalnych składowych danych. Specyfikacja mutable umożliwia treści wyrażenia lambda modyfikowanie zmiennych przechwyconych przez wartość. Niektóre z przykładów w dalszej części tego artykułu pokazują, jak używać polecenia mutable.

Specyfikacja wyjątku

Możesz użyć specyfikacji wyjątku noexcept , aby wskazać, że wyrażenie lambda nie zgłasza żadnych wyjątków. Podobnie jak w przypadku zwykłych funkcji kompilator języka Microsoft C++ generuje ostrzeżenie C4297 , jeśli wyrażenie lambda deklaruje noexcept specyfikację wyjątku, a treść lambda zgłasza wyjątek, jak pokazano poniżej:

// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
   []() noexcept { throw 5; }();
}

Aby uzyskać więcej informacji, zobacz Specyfikacje wyjątków (throw).

Typ Zwrot

Zwracany typ wyrażenia lambda jest automatycznie wywoływany. Nie musisz używać słowa kluczowego auto , chyba że określisz końcowy typ zwracany. Końcowy typ zwracany przypomina część zwracaną funkcji lub składowej zwykłej funkcji. Jednak zwracany typ musi być zgodny z listą parametrów i przed typem zwrotnym należy dołączyć końcowe słowo kluczowe -> return-type.

Można pominąć część zwracaną wyrażenia lambda, jeśli treść lambda zawiera tylko jedną instrukcję zwracaną. Lub, jeśli wyrażenie nie zwraca wartości. Jeśli treść lambda zawiera jedną instrukcję zwracaną, kompilator deduuje typ zwracany z typu wyrażenia zwracanego. W przeciwnym razie kompilator deduuje zwracany typ jako void. Rozważ następujące przykładowe fragmenty kodu, które ilustrują tę zasadę:

auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing
                                  // return type from braced-init-list isn't valid

Wyrażenie lambda może generować inne wyrażenie lambda jako wartość zwracaną. Aby uzyskać więcej informacji, zobacz "Wyrażenia lambda o wyższej kolejności" w temacie Przykłady wyrażeń lambda.

Treść lambda

Treść lambda wyrażenia lambda jest instrukcją złożoną. Może zawierać wszystko, co jest dozwolone w treści zwykłej funkcji lub funkcji składowej. Treść zarówno funkcji zwykłej, jak i wyrażenia lambda może uzyskiwać dostęp do tego rodzaju zmiennych:

  • Przechwycone zmienne z otaczającego zakresu zgodnie z wcześniejszym opisem.

  • Parameters.

  • Zmienne zadeklarowane lokalnie.

  • Składowe danych klasy, gdy zadeklarowane wewnątrz klasy i this są przechwytywane.

  • Dowolna zmienna, która ma czas trwania magazynu statycznego — na przykład zmienne globalne.

Poniższy przykład zawiera wyrażenie lambda, które jawnie przechwytuje zmienną n według wartości i niejawnie przechwytuje zmienną m według odwołania:

// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include <iostream>
using namespace std;

int main()
{
   int m = 0;
   int n = 0;
   [&, n] (int a) mutable { m = ++n + a; }(4);
   cout << m << endl << n << endl;
}
5
0

Ponieważ zmienna n jest przechwytywana przez wartość, jego wartość pozostaje 0 po wywołaniu wyrażenia lambda. Specyfikacja mutable umożliwia n modyfikację w obrębie lambda.

Wyrażenie lambda może przechwytywać tylko zmienne, które mają automatyczny czas trwania magazynu. Można jednak użyć zmiennych, które mają czas trwania magazynu statycznego w treści wyrażenia lambda. W poniższym przykładzie generate użyto funkcji i wyrażenia lambda w celu przypisania wartości do każdego elementu w vector obiekcie. Wyrażenie lambda modyfikuje zmienną statyczną, aby wygenerować wartość następnego elementu.

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

Aby uzyskać więcej informacji, zobacz generowanie.

Poniższy przykład kodu używa funkcji z poprzedniego przykładu i dodaje przykład wyrażenia lambda używającego algorytmu generate_nbiblioteki standardowej języka C++. To wyrażenie lambda przypisuje element vector obiektu do sumy poprzednich dwóch elementów. Słowo mutable kluczowe jest używane tak, aby treść wyrażenia lambda mogła modyfikować jego kopie zmiennych x zewnętrznych i y, które wyrażenie lambda przechwytuje według wartości. Ponieważ wyrażenie lambda przechwytuje oryginalne zmienne x i y według wartości, ich wartości pozostają 1 po wykonaniu wyrażenia lambda.

// compile with: /W4 /EHsc
#include <algorithm>
#include <iostream>
#include <vector>
#include <string>

using namespace std;

template <typename C> void print(const string& s, const C& c) {
    cout << s;

    for (const auto& e : c) {
        cout << e << " ";
    }

    cout << endl;
}

void fillVector(vector<int>& v)
{
    // A local static variable.
    static int nextValue = 1;

    // The lambda expression that appears in the following call to
    // the generate function modifies and uses the local static
    // variable nextValue.
    generate(v.begin(), v.end(), [] { return nextValue++; });
    //WARNING: this isn't thread-safe and is shown for illustration only
}

int main()
{
    // The number of elements in the vector.
    const int elementCount = 9;

    // Create a vector object with each element set to 1.
    vector<int> v(elementCount, 1);

    // These variables hold the previous two elements of the vector.
    int x = 1;
    int y = 1;

    // Sets each element in the vector to the sum of the
    // previous two elements.
    generate_n(v.begin() + 2,
        elementCount - 2,
        [=]() mutable throw() -> int { // lambda is the 3rd parameter
        // Generate current value.
        int n = x + y;
        // Update previous two values.
        x = y;
        y = n;
        return n;
    });
    print("vector v after call to generate_n() with lambda: ", v);

    // Print the local variables x and y.
    // The values of x and y hold their initial values because
    // they are captured by value.
    cout << "x: " << x << " y: " << y << endl;

    // Fill the vector with a sequence of numbers
    fillVector(v);
    print("vector v after 1st call to fillVector(): ", v);
    // Fill the vector with the next sequence of numbers
    fillVector(v);
    print("vector v after 2nd call to fillVector(): ", v);
}
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34
x: 1 y: 1
vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9
vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18

Aby uzyskać więcej informacji, zobacz generate_n.

constexpr wyrażenia lambda

Program Visual Studio 2017 w wersji 15.3 lub nowszej (dostępny w /std:c++17 trybie i nowszych): wyrażenie lambda można zadeklarować jako constexpr (lub użyć go w wyrażeniu stałym), gdy inicjowanie każdego przechwyconego lub wprowadzonego elementu członkowskiego danych jest dozwolone w wyrażeniu stałym.

    int y = 32;
    auto answer = [y]() constexpr
    {
        int x = 10;
        return y + x;
    };

    constexpr int Increment(int n)
    {
        return [n] { return n + 1; }();
    }

Lambda jest niejawnie constexpr , jeśli jego wynik spełnia wymagania constexpr funkcji:

    auto answer = [](int n)
    {
        return 32 + n;
    };

    constexpr int response = answer(10);

Jeśli lambda jest niejawnie lub jawnie constexpr, konwersja na wskaźnik funkcji tworzy constexpr funkcję:

    auto Increment = [](int n)
    {
        return n + 1;
    };

    constexpr int(*inc)(int) = Increment;

specyficzne dla firmy Microsoft

Lambdas nie są obsługiwane w następujących jednostkach ref classzarządzanych środowiska uruchomieniowego języka wspólnego (CLR): , ref struct, value classlub value struct.

Jeśli używasz modyfikatora specyficznego dla firmy Microsoft, takiego jak __declspec, możesz wstawić go do wyrażenia lambda bezpośrednio po parameter-declaration-clause. Na przykład:

auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

Aby określić, czy określony modyfikator jest obsługiwany przez lambda, zobacz artykuł o modyfikatorze w sekcji modyfikatorów specyficznych dla firmy Microsoft.

Program Visual Studio obsługuje funkcje lambda języka C++11 Standard i bezstanowe lambda. Bezstanowa lambda jest konwertowana na wskaźnik funkcji, który używa dowolnej konwencji wywoływania.

Zobacz też

Dokumentacja języka C++
Obiekty funkcji w standardowej bibliotece C++
Wywołanie funkcji
for_each