Лямбда-функции появились в C++11. Они представляют собой анонимные функции, которые можно определить в любом месте программы, подходящем по смыслу.
Приведу пример простейшей лямбда функции:
1 2 |
auto myLambda = [](){ std::cout << "Hello, lambda!" << std::endl; }; myLambda(); |
Выражение auto myLambda означает объявление переменной с автоматически определяемым типом. Крайне удобная конструкция C++11, которая позволяет сделать ваш код более лаконичным и устойчивым к изменениям. Настоящий тип лямбда-функции слишком сложный, поэтому набирать его нецелесообразно.
РЕКОМЕНДУЕМ: Десять советов по созданию гибкого программного кода
Непосредственное объявление лямбда-функции [](){ std::cout << "Hello, lambda!" << std::endl; } состоит из трех частей. Первая часть (квадратные скобки []) позволяет привязывать переменные, доступные в текущей области видимости. Вторая часть (круглые скобки ()) указывает список принимаемых параметров лямбда-функции. Третья часть (в фигурных скобках {}) содержит тело лямбда-функции.
Вызов определенной лямбда-функции ничем не отличается от вызова обычной функции: myLambda(). В нашем случае на консоль будет выведено сообщение:
1 |
"Hello, lambda!" |
Прежде, чем перейти к более сложным примерам лямбда-функций, определим вспомогательную шаблонную функцию:
1 2 3 4 |
template< typename Func > void call( Func func ) { func( -5 ); } |
В качестве аргумента она принимает любой объект, который можно вызвать с аргументом -5. Более подробно о создании таких функций мы говорили, когда рассматривали указатели на функции в C++. Мы будем передавать в call() наши лямбда-функции для запуска.
Сначала просто выведем переданное лямбда-функции значение:
1 2 3 4 5 |
call( []( int val ) { // Параметр val == -5, т.е. соответствует переданному значению std::cout << val << std::endl; // --> -5 } ); |
Теперь рассмотрим возможность «замыкания», т.е. передадим в лямбда-функцию значение локальной переменной по значению:
1 2 3 4 5 6 7 |
int x = 5; call( [ x ]( int val ) { // x == 5, но параметр передается по значению std::cout << x + val << std::endl; // --> 0 // x = val; - Не компилируется. Изменять значение x мы не можем } ); |
Но если мы хотим изменить значение переменной внутри лямбда-функции, то можем передать его по ссылке:
1 2 3 4 5 6 7 8 9 10 |
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 |
Обратите внимание на побочный эффект от связывания переменных с лямбда-функцией по ссылке:
1 2 3 4 5 6 |
int x = 5; auto refLambda = [ &x ](){ std::cout << x << std::endl; }; refLambda(); // --> 5 x = 94; // Значение поменялось, что скажется и на лямбда-функции! refLambda(); // --> 94 |
Будьте особенно аккуратны с привязкой параметров по ссылке, когда работаете с циклами. Чтобы не получилось, что все созданные лямбда-функции работали с одним и тем же значением, когда должны иметь собственные копии.
Привязку можно осуществлять по любому числу переменных, комбинируя как передачу по значению, так и по ссылке:
1 2 3 4 5 6 7 8 9 10 |
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! } ); |
Если требуется привязать сразу все переменные, то можно использовать следующие конструкции:
1 2 3 4 5 6 7 8 9 |
call( // Привязываем все переменные в области видимости по значению [ = ]( int val ){ /* ... */ } ); call( // Привязываем все переменные в области видимости по ссылке [ & ]( int val ){ /* ... */ } ); |
Допустимо и комбинирование:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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. Большинство функций этой библиотеки принимают аргумент-предикат. Такой аргумент позволяет контролировать те или иные аспекты алгоритма.
Конечно, этот аргумент не обязан быть лямбда-функцией, но часто их применение оказывается оправданным. Например:
1 2 3 |
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 << " "; } ); |
Этот код интуитивно понятен, поэтому вы сами с ним легко разберетесь без моих пояснений. Приведу лишь то, что он выведет на консоль:
1 |
5 7 9 11 |
В своих программах вы тоже можете создавать универсальные функции, для которых управление ходом выполнения осуществляется с помощью функциональных объектов и лямбда-функций в частности.
РЕКОМЕНДУЕМ: Основные структуры данных в C++