IT Notes

Лямбда-функции в C++

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

Приведу пример простейшей лямбда функции:

auto myLambda = [](){ std::cout << "Hello, lambda!" << std::endl; };
myLambda();

Выражение auto myLambda означает объявление переменной с автоматически определяемым типом. Крайне удобная конструкция C++11, которая позволяет сделать ваш код более лаконичным и устойчивым к изменениям. Настоящий тип лямбда-функции слишком сложный, поэтому набирать его нецелесообразно.

Непосредственное объявление лямбда-функции [](){ std::cout << "Hello, lambda!" << std::endl; } состоит из трех частей. Первая часть (квадратные скобки []) позволяет привязывать переменные, доступные в текущей области видимости. Вторая часть (круглые скобки ()) указывает список принимаемых параметров лямбда-функции. Третья часть (в фигурных скобках {}) содержит тело лямбда-функции.

Вызов определенной лямбда-функции ничем не отличается от вызова обычной функции: myLambda(). В нашем случае на консоль будет выведено сообщение:

"Hello, lambda!"

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

template< typename Func >
void call( Func func ) {
    func( -5 );
}

В качестве аргумента она принимает любой объект, который можно вызвать с аргументом -5. Более подробно о создании таких функций мы говорили, когда рассматривали указатели на функции в C++. Мы будем передавать в call() наши лямбда-функции для запуска.

Сначала просто выведем переданное лямбда-функции значение:

call(
    []( int val ) { // Параметр val == -5, т.е. соответствует переданному значению
        std::cout << val << std::endl; // --> -5
    }
);

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

int x = 5;
call(
    [ x ]( int val ) { // x == 5, но параметр передается по значению
        std::cout << x + val << std::endl; // --> 0
        // x = val; - Не компилируется. Изменять значение x мы не можем
    }
);

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

int x = 33;
call(
    [ &x ]( int val ) { // Теперь x передается по ссылке
        std::cout << x << std::endl; // --> 33
        x = val; // OK!
        std::cout << x << std::endl; // --> -5
    }
);
// Значение изменилось:
std::cout << x << std::endl; // --> -5

Обратите внимание на побочный эффект от связывания переменных с лямбда-функцией по ссылке:

int x = 5;
auto refLambda = [ &x ](){ std::cout << x << std::endl; };
refLambda(); // --> 5
x = 94;
// Значение поменялось, что скажется и на лямбда-функции!
refLambda(); // --> 94

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

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

int y = 55;
int z = 94;
call(
    [ y, &z ]( int val ) {
        std::cout << y << std::endl; // --> 55
        std::cout << z << std::endl; // --> 94
        // y = val; - Нельзя
        z = val; // OK!
    }
);

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

call(
    // Привязываем все переменные в области видимости по значению
    [ = ]( int val ){ /* … */ }
);

call(
    // Привязываем все переменные в области видимости по ссылке
    [ & ]( int val ){  /* … */ }
);

Допустимо и комбинирование:

int x = 21;
int y = 55;
int z = 94;
int w = 42;

call(
    // Привязываем x по значению, а все остальное по ссылке
    [ &, x ]( int val ){ /* … */ }
);

call(
    // Привязываем y по ссылке, а все остальное по значению
    [ =, &y ]( int val ){  /* … */ }
);

call(
    // Привязываем x и w по ссылке, а все остальное по значению
    [ =, &x, &w ]( int val ){  /* … */ }
);

Однако замечу, что на практике лучше не использовать обобщенное привязывание через = и &, а явно обозначать необходимые переменные по одной. Иначе могут возникнуть загадочные ошибки из-за конфликтов имен.

Когда использовать лямбда-функции?

Один из лучших примеров правильного использования лямбда-функций связан с библиотекой алгоритмов stl. Большинство функций этой библиотеки принимают аргумент-предикат. Такой аргумент позволяет контролировать те или иные аспекты алгоритма.

Конечно, этот аргумент не обязан быть лямбда-функцией, но часто их применение оказывается оправданным. Например:

std::vector< int > v = { 2, 4, 5, 6, 7, 9, 11, 14 };
auto end = std::remove_if( v.begin(), v.end(), []( int x ) { return x % 2 == 0; } );
std::for_each( v.begin(), end, []( int x ) { std::cout << x << " "; } );

Этот код интуитивно понятен, поэтому вы сами с ним легко разберетесь без моих пояснений. Приведу лишь то, что он выведет на консоль:

5 7 9 11

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

Похожие публикации

Комментарии

"Интуитивно понятна" - это inline функция. А нахрена заморачиваться со всякими лямбда - нифига не понятно. Хотя для "говнокода" - в самый раз. Имхо.

Anonymous:

"Интуитивно понятна" - это inline функция. А нахрена заморачиваться со всякими лямбда - нифига не понятно. Хотя для "говнокода" - в самый раз. Имхо.

Согласен, что пример несколько притянут. Может потом подберу более подходящий. Приведенный является вариацией примера для std::remove_if с cplusplus.com.

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

Лямда фнкция дает искушение написать оные 100500 раз в коде программы и превратить оную в превдосложный-мегакод. Вместо того, чтобы написать отдельно функцию, мы ее "по-быстрому" слабаем в теле другой функции. По принципу; "не успеваю сделать нормально - план горит". Имхо.

Anonymous:

Лямда фнкция дает искушение написать оные 100500 раз в коде программы и превратить оную в превдосложный-мегакод. Вместо того, чтобы написать отдельно функцию, мы ее "по-быстрому" слабаем в теле другой функции. По принципу; "не успеваю сделать нормально - план горит". Имхо.

Ну да. Тут, как и в любом деле, нужно знать меру. Если можно написать более общее решение, которое получится использовать в коде многократно, то так и следует поступать.

В частности, для примера из статьи с проверкой на четность можно создать функтор на подобии <typename T>IsDivisibleBy. Чтобы использовать его по нужному месту с аргументом:

std::remove_if( v.begin(), v.end(), IsDivisibleBy< int >( 2 ) );

В этом смысле злоупотребление лямбда-функциями аналогично использованию "волшебных чисел". С другой стороны, если есть уверенность, что требуется очень специфическая функция, которая больше в коде нигде не понадобится в обозримом будущем, то лямбдой вполне можно обойтись (например, для какого-нибудь callback'а). Хотя, конечно, все часто идет не по плану, и функция где-нибудь может понадобиться вновь. В этом случае дублированием заниматься неуместно, и без рефакторинга не обойтись.

Очень интересно!

int array[] = { 1, 2, 3, 4, 5, 6 };

int x = 5[array];

Как это работает? Где тело лямбды и что за синтакс индекса перед массивом?

В приведенном фрагменте кода лямбды не используются. А запись

int x = 5[array];

можно представить в виде:

int x = *( 5 + array ); // или *( array + 5 )

что равносильно:

int x = array[5]; // т.е. x = 6

лямда-функции достаточно удобно использовать в qt с их сигналами-слотами. Так вот вместо слота порой удобно использовать ЛФ, что, кстати, и предлагается в некоторых примерах от qt