Поделиться через


Лямбда-выражения в 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++. Скобки относятся к списку параметров. Изменяемое ключевое слово является необязательным. throw() — это необязательная спецификация исключения. -> int — это необязательный тип возвращаемого значения. Лямбда-тело состоит из инструкции внутри фигурных фигурных скобок, или возвращать x+y; Они более подробно описаны после изображения.

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

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

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

  4. Необязательный параметр спецификации исключений .

  5. конечная возвращаемая тип необязательный.

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

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

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

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

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

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

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

Если предложение записи содержит запись по умолчанию &, то в записи этого предложения записи не может быть &identifierидентификатора. Аналогичным образом, если предложение записи содержит запись по умолчанию =, то запись этого предложения не может иметь форму =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
}

Запись, за которой следует многоточие, является расширением пакета, как показано в этом примере шаблона variadic:

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

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

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

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

При использовании предложения захвата рекомендуется учитывать эти моменты, особенно при использовании лямбда-лямбда-кодов с несколькими потоками:

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

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

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

Обобщенное запись (C++14)

В C++14 можно ввести и инициализировать новые переменные в предложении записи без необходимости существовать эти переменные в лямбда-функции в заключающей области. Инициализация может быть выражена в качестве любого произвольного выражения. Тип новой переменной определяется типом, который создается выражением. Эта функция позволяет записывать переменные, доступные только для перемещения (например 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.

Мутируемая спецификация

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

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

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

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

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

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

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

Можно опустить часть возвращаемого типа лямбда-выражения, если лямбда-текст содержит только одну инструкцию возвращаемого значения. Или, если выражение не возвращает значение. Если тело лямбда-выражения содержит один оператор 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 structvalue classили value 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