Лямбда-выражения в C++

В C++11 и более поздних версиях лямбда-выражение, часто называемое лямбда-выражением, является удобным способом определения анонимного объекта функции ( закрытия) непосредственно в месте, где он вызывается или передается в качестве аргумента функции. Обычно лямбда-выражения используются для инкапсуляции нескольких строк кода, которые передаются алгоритмам или асинхронным функциям. В этой статье описывается, что такое лямбда-выражения, и сравнивается их с другими методами программирования. Здесь описываются их преимущества и приводятся некоторые основные примеры.

Части лямбда-выражения

Ниже приведена простая лямбда-выражение, передаваемая в качестве третьего аргумента функции std::sort() :

#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
    );
}

На этом рисунке показаны части лямбда-синтаксиса:

Схема, идентифицирующая различные части лямбда-выражения.

Пример лямбда-выражения — [=]() mutable throw() -> int { return x+y; } [=] — это предложение отслеживания; также известный как лямбда-введение в спецификации C++. Круглые скобки относятся к списку параметров. Ключевое слово mutable является необязательным. throw() — необязательная спецификация исключения. -> int является необязательным конечным типом возвращаемого значения. Тело лямбда-выражения состоит из оператора внутри фигурных скобок или возврата x+y; Они подробно описаны на рисунке.

  1. Предложение capture (также известное как лямбда-вводное выражение в спецификации C++).

  2. список параметров Дополнительные. (Также называется лямбда-декларатором)

  3. изменяемая спецификация Дополнительные.

  4. спецификация исключений Дополнительные.

  5. тип возвращаемого значения Дополнительные.

  6. Лямбда-тело.

Предложение Capture

Лямбда-выражение может вводить новые переменные в своем теле (в C++14), а также получать доступ к переменным из окружающей области или захватывать их. Лямбда-выражение начинается с предложения capture. Он указывает, какие переменные записываются, а также указывает, выполняется ли запись по значению или по ссылке. Доступ к переменным с префиксом амперсанда (&) можно получить по ссылке, а переменным, у которых нет этого префикса, — по значению.

Пустое предложение фиксации ([ ]) показывает, что тело лямбда-выражения не осуществляет доступ к переменным во внешней области видимости.

Вы можете использовать режим записи по умолчанию, чтобы указать, как захватывать любые внешние переменные, на которые ссылается лямбда-текст: [&] означает, что все переменные, на которые вы ссылаетесь, захватываются по ссылке, а [=] означает, что они фиксируются по значению. Можно сначала использовать режим фиксации по умолчанию, а затем применить для определенных переменных другой режим. Например, если тело лямбда-выражения осуществляет доступ к внешней переменной total по ссылке, а к внешней переменной factor по значению, следующие предложения фиксации эквивалентны:

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

При использовании параметра capture-default записываются только переменные, упомянутые в лямбда-тексте.

Если предложение отслеживания содержит свойство capture-default &, то идентификатор в записи этого предложения отслеживания не может иметь форму &identifier. Аналогичным образом, если предложение отслеживания содержит свойство capture-default =, то никакой захват этого предложения отслеживания не может иметь форму =identifier. Идентификатор или this не может отображаться более одного раза в предложении отслеживания. В следующем фрагменте кода показаны некоторые примеры:

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
}

Захват с последующим многоточием представляет собой расширение пакета, как показано в следующем примере с вариадным шаблоном :

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

Чтобы использовать лямбда-выражения в теле функции-члена класса, передайте this указатель на предложение capture, чтобы предоставить доступ к функциям-членам и членам данных включаемого класса.

Visual Studio 2017 версии 15.3 и более поздних версий (доступно в /std:c++17 режиме и более поздних версиях): this указатель можно записать по значению, указав *this в предложении capture. Захват по значению копирует все закрытие на каждый сайт вызова, где вызывается лямбда-выражение. (Закрытие — это объект анонимной функции, инкапсулирующий лямбда-выражение.) Запись по значению полезна, когда лямбда-выражения выполняются в параллельных или асинхронных операциях. Это особенно полезно для определенных архитектур оборудования, таких как NUMA.

Пример использования лямбда-выражений с функциями-членами класса см. в разделе "Пример: использование лямбда-выражения в методе" статьи Примеры лямбда-выражений.

При использовании предложения capture рекомендуется учитывать следующие моменты, особенно при использовании лямбда-выражений с многопотоковой:

  • Записи ссылок можно использовать для изменения переменных за пределами, а записи значений — нет. (mutable позволяет изменять копии, но не оригиналы.)

  • Записи ссылок отражают обновления переменных за пределами, а записи значений — нет.

  • Фиксация ссылки вводит зависимость от времени существования, тогда как фиксация значения не обладает зависимостями от времени существования. Это особенно важно, когда лямбда-выражение выполняется асинхронно. Если записать локальный объект по ссылке в асинхронном лямбда-коде, он может быть легко удален к моменту выполнения лямбда-выражения. Код может привести к нарушению доступа во время выполнения.

Обобщенная фиксация (C++14)

В C++14 можно вводить и инициализировать новые переменные в предложении capture, не требуя, чтобы эти переменные существовали во включающей области лямбда-функции. Инициализация может быть выражена в качестве любого произвольного выражения. Тип новой переменной определяется типом, который создается выражением. Эта функция позволяет захватывать переменные только для перемещения (например, std::unique_ptr) из окружающей области и использовать их в лямбда-выражениях.

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

Список параметров

Лямбда-выражения могут захватывать переменные и принимать входные параметры. Список параметров (лямбда-декларатор в стандартном синтаксисе) является необязательным и в большинстве аспектов напоминает список параметров для функции.

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

В C++14, если тип параметра является универсальным auto , в качестве описателя типа можно использовать ключевое слово . Это ключевое слово указывает компилятору создать оператор вызова функции в качестве шаблона. Каждый экземпляр auto в списке параметров эквивалентен параметру отдельного типа.

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

Лямбда-выражение может принимать другое лямбда-выражение в качестве своего аргумента. Дополнительные сведения см. в разделе "Лямбда-выражения более высокого порядка" статьи Примеры лямбда-выражений.

Так как список параметров является необязательным, можно опустить пустые круглые скобки, если не передать аргументы лямбда-выражению, а его лямбда-декларатор не содержит спецификацию исключения, тип возвращаемого значения или mutable.

Изменяемая спецификация

Как правило, оператор вызова функции лямбда-выражения констант по значению mutable , но использование ключевого слова отменяет это. Он не создает изменяемые элементы данных. Спецификация mutable позволяет тексту лямбда-выражения изменять переменные, захваченные значением. В некоторых примерах далее в этой статье показано, как использовать mutable.

Спецификация исключений

Можно использовать спецификацию исключений noexcept , чтобы указать, что лямбда-выражение не создает никаких исключений. Как и в случае с обычными функциями, компилятор Microsoft C++ создает предупреждение C4297 , если лямбда-выражение объявляет спецификацию noexcept исключения, а тело лямбда-выражения создает исключение, как показано ниже:

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

Дополнительные сведения см. в разделе Спецификации исключений (исключение).

Возвращаемый тип

Возвращаемый тип лямбда-выражения выводится автоматически. Не нужно использовать ключевое слово , auto если не указан тип конечного возвращаемого значения. Конечный тип возврата напоминает часть возвращаемого типа обычной функции или функции-члена. Однако тип возвращаемого значения следует списку параметров, и необходимо включить ключевое слово -> элемента trailing-return-type перед типом возвращаемого значения.

Можно опустить часть возвращаемого типа лямбда-выражения, если тело лямбда-выражения содержит только один оператор return. Или, если выражение не возвращает значение. Если тело лямбда-выражения содержит один оператор return, компилятор выводит тип возвращаемого значения из типа возвращаемого выражения. В противном случае компилятор выводит тип возвращаемого значения как void. Рассмотрим следующий пример фрагментов кода, иллюстрирующих этот принцип:

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

Лямбда-выражение может создавать другое лямбда-выражение в качестве своего возвращаемого значения. Дополнительные сведения см. в разделе "Лямбда-выражения более высокого порядка" статьи Примеры лямбда-выражений.

Тело лямбда-выражения

Лямбда-тело лямбда-выражения является составным оператором. Он может содержать все, что разрешено в теле обычной функции или функции-члена. Тело обычной функции и лямбда-выражения может осуществлять доступ к следующим типам переменных:

  • Фиксированные переменные из внешней области видимости (см. выше).

  • Параметры.

  • Локально объявленные переменные.

  • Члены данных класса, если они объявлены внутри класса и this захватываются.

  • Любая переменная со статической длительностью хранения, например глобальные переменные.

В следующем примере содержится лямбда-выражение, которое явно фиксирует переменную n по значению и неявно фиксирует переменную m по ссылке.

// 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

Поскольку переменная n фиксируется по значению, ее значение после вызова лямбда-выражения остается равным 0. Спецификация mutable позволяет n изменяться в лямбда-выражении.

Лямбда-выражение может записывать только переменные с автоматической длительностью хранения. Однако можно использовать переменные со статической длительностью хранения в теле лямбда-выражения. В следующем примере функция generate и лямбда-выражение используются для присвоения значения каждому элементу объекта vector. Лямбда-выражение изменяет статическую переменную для получения значения следующего элемента.

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
}

Дополнительные сведения см. в разделе Создание.

В следующем примере кода используется функция из предыдущего примера и добавляется пример лямбда-выражения, использующего алгоритм generate_nстандартной библиотеки C++. Это лямбда-выражение назначает элемент объекта vector сумме предыдущих двух элементов. Ключевое mutable слово используется для того, чтобы текст лямбда-выражения может изменять свои копии внешних переменных x и y, которые лямбда-выражение захватывает по значению. Поскольку лямбда-выражение захватывает исходные переменные x и y по значению, их значения остаются равными 1 после выполнения лямбда-выражения.

// 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

Дополнительные сведения см. в разделе generate_n.

constexpr Лямбда-выражения

Visual Studio 2017 версии 15.3 и более поздних версий (доступно в /std:c++17 режиме и более поздних версиях). Вы можете объявить лямбда-выражение как constexpr (или использовать его в константном выражении), если разрешена инициализация каждого записанного или введенного элемента данных в константном выражении.

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

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

Лямбда неявно constexpr , если его результат соответствует требованиям constexpr функции:

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

    constexpr int response = answer(10);

Если лямбда-выражение является неявным или явным образом constexpr, преобразование в указатель функции создает функцию constexpr :

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

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

Специально для систем Майкрософт

Лямбда-выражения не поддерживаются в следующих управляемых сущностях среды CLR: ref class, ref struct, или value classvalue struct.

Если вы используете модификатор майкрософт, __declspecнапример , его можно вставить в лямбда-выражение сразу после parameter-declaration-clause. Пример:

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

Чтобы определить, поддерживается ли определенный модификатор лямбда-выражениями, см. статью о модификаторе в разделе Модификаторы, относящиеся к Майкрософт .

Visual Studio поддерживает лямбда-функции C++11 Standard и лямбда-выражения без отслеживания состояния. Лямбда-выражения без отслеживания состояния можно преобразовать в указатель функции, использующий соглашение о произвольных вызовах.

См. также

Справочник по языку C++
Объекты-функции в стандартной библиотеке C++
Вызов функции
for_each