Лямбда-выражения. Объявление и вызов

Практический курс по C/C++: https://stepik.org/course/193691

В стандарте компилятора С++11 появились, так называемые, лямбда-функции (еще говорят лямбда-выражения или анонимные функции). Что это за функции и зачем они нужны? Если очень кратко, то лямбда-выражение позволяет создавать простой объект-функцию в любом допустимом месте программы. И одно из таких допустимых мест – аргумент обычной функции. Давайте я все детально поясню на конкретных примерах. А начнем, конечно же, со способа объявления и вызова таких лямбда-функций.

Общий синтаксис их определения следующий:

[] ([параметры]) { <операторы тела функции>}

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

В самом простом варианте лямбда-выражение можно записать в виде:

#include <iostream>
 
using std::cout;
using std::cin;
using std::endl;
 
int main()
{
    [](int a) {
        cout << "Lambda-function: " << a << endl;
    };
 
    return 0;
}

Здесь в круглых скобках указан один параметр, а в фигурных – один оператор с выводом строки в выходной поток.

Если сейчас выполнить программу, то она успешно скомпилируется, но в консоль ничего выведено не будет. Почему так произошло? Да, по той причине, что мы объявили функцию, но не вызвали ее. Но как вызывать функцию, у которой нет имени? В самом простом варианте можно добавить вызов сразу после ее определения, например, так:

    [](int a) {
        cout << "Lambda-function: " << a << endl;
    } (10);

Но смысла от такой операции большого не будет. Поэтому на практике чаще всего созданный объект-функцию присваивают какой-либо переменной. Мы сделаем это следующим образом:

    auto r = [](int a) {
        cout << "Lambda-function: " << a << endl;
    };
 
    r(10);

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

auto s = r;
s(15);

При этом s будет уже другим объектом-функцией. В момент инициализации происходит копирование одного объекта в другой.

Или, эту же инициализацию можно прописать в виде:

auto s {r};

Или же, явно прописать еще одно лямбда-выражение:

    auto s { [](const char* msg, double& x) {
            cout << msg << endl;
            x++;
        } };
 
    double b = 0;
    s("increment", b);

Конечно, лямбда-выражение может возвращать произвольные данные с помощью оператора return, как это делают обычные функции. Например, ту же самую функцию s мы можем записать в виде:

    auto s { [](const char* msg, double x) {
            cout << msg << endl;
            return ++x;
        } };
 
    double b = s("increment", 4);

В результате, переменная b примет значение 5.

Или, более простой пример:

auto sum2 { [](int a, int b) {return a+b;} };

Получили объект-функцию для сложения двух целых чисел. Если мы посмотрим на сформированный тип для переменной sum2, то увидим:

int sum2(int a, int b)

При необходимости, возвращаемый тип можно указывать явно. Для этого, после круглых скобок ставится оператор -> с указанием нужного типа. Например:

auto sum2 { [](int a, int b) -> double {return a+b;} };

Получим вычисленный тип вида:

double sum2(int a, int b)

Начиная со стандарта С++14 в лямбда-выражениях можно использовать вычисляемые типы у параметров. Например:

auto sum2 { [](auto a, auto b) -> double {return a+b;} };

и, соответственно, вызывать функцию, например, так:

 double res_1 = sum2(3, 5);
 double res_2 = sum2(3.4, 5.3);
 cout << res_1 << " " << res_2 << endl;

Можно пойти еще дальше у всех типов прописать ключевое слово auto, получим:

auto sum2 { [](auto a, auto b) -> auto {return a+b;} };

В результате объект sum2 можно прописывать для сложения любых допустимых данных. Например:

std::string res_3 = sum2(std::string("hello, "), std::string("world!"));
cout << res_3 << endl;

Лямбда-выражения в аргументах функций

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

Объявим функцию с именем show_ar, которая выводит целочисленные значения переданного ей массива ar длиной length:

void show_ar(const int* ar, size_t length, bool (*filter_func)(int) = nullptr)
{
    for(int i = 0;i < length; ++i) {
        if(filter_func != nullptr) {
            if(filter_func(ar[i]))
                cout << ar[i] << " ";
        }
        else
            cout << ar[i] << " ";
    }
}

Последний параметр представляет собой указатель на функцию со значением по умолчанию nullptr. В этом случае в консоль выводится весь массив от начала до конца. Например, так:

int main()
{
    int data[] {1, 2, 3, 4, 5, 6, 7, 8};
    show_ar(data, sizeof(data)/sizeof(*data));
 
    return 0;
}

Но третьим аргументом можно передать функцию, которая позволит нам фильтровать данные и отображать только нужные значения массива, например, только четные. Для этого воспользуемся лямбда-выражением и пропишем его следующим образом прямо в момент вызова функции show_ar():

show_ar(data, sizeof(data)/sizeof(*data), [](int x) {return x % 2 == 0;});

Анонимная функция возвращает истину для четных значений и ложь для нечетных. В итоге видим в консоли только четные значения элементов массива.

Видите, как удобно определять произвольные критерии выбора числовых значений с помощью лямбда-функций? В результате, функция show_ar() стала в некотором смысле универсальной. Ее можно вызвать для отображения массива целых чисел с любым критерием отбора. Например, всех чисел кратных трем:

show_ar(data, sizeof(data)/sizeof(*data), [](int x) {return x % 3 == 0;});

Преимущество лямбда-выражения в этом примере перед обычной функцией еще и в том, что это выражение для функции существует лишь при вызове show_ar() и автоматически пропадает после вызова. То есть, имеем экономию памяти и не нагромождаем программу лишними объявлениями функций.

На следующем занятии продолжим эту тему и увидим, зачем нужны квадратные скобки при определении лямбда-выражений.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме