Введение
Мы разобрали указатели на функции как способ передачи поведения как параметра — лямбда-выражения (C++11) дают значительно более гибкую, современную альтернативу, способную «захватывать» переменные из окружающего контекста, чего указатели на функции не умеют принципиально, и в этой статье разберём лямбды и различные способы захвата.
Концепция
Лямбда-выражение — анонимная (не имеющая явного имени) функция, определяемая непосредственно в месте использования, синтаксис которой включает список захвата ([...], определяющий, какие переменные из окружающего контекста доступны внутри лямбды и как — по значению или по ссылке), список параметров, и тело функции. Захват по значению ([x]) создаёт копию переменной x на момент создания лямбды, и последующие изменения оригинальной переменной x за пределами лямбды не повлияют на эту захваченную копию — захват по ссылке ([&x]) даёт лямбде прямой доступ к оригинальной переменной, и изменения, видимые снаружи лямбды (или сделанные самой лямбдой), отражаются на одной и той же, общей переменной. [=] и [&] — сокращённые формы, захватывающие ВСЕ упомянутые в теле лямбды внешние переменные по значению или по ссылке соответственно, без необходимости перечислять каждую переменную явно по отдельности.
Пример кода
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
int threshold = 10;
auto isAboveThreshold = [threshold](int value) { // захват threshold ПО ЗНАЧЕНИЮ
return value > threshold;
};
std::vector<int> numbers = {5, 15, 8, 20, 3};
int count = std::count_if(numbers.begin(), numbers.end(), isAboveThreshold);
std::cout << "Чисел больше " << threshold << ": " << count << std::endl;
return 0;
}
// Захват по значению против захвата по ссылке — принципиально разное поведение
#include <iostream>
int main()
{
int counter = 0;
auto incrementByValue = [counter]() mutable { // mutable — необходимо для ИЗМЕНЕНИЯ захваченной по значению копии
counter++; // изменяет только ВНУТРЕННЮЮ копию лямбды, НЕ оригинальный counter
std::cout << "Внутри (by value): " << counter << std::endl;
};
auto incrementByReference = [&counter]() {
counter++; // изменяет ОРИГИНАЛЬНУЮ переменную counter напрямую
std::cout << "Внутри (by reference): " << counter << std::endl;
};
incrementByValue(); // "Внутри (by value): 1" — но оригинальный counter всё ещё 0!
incrementByValue(); // "Внутри (by value): 2" — внутренняя копия растёт независимо
std::cout << "Оригинал после by value: " << counter << std::endl; // 0 — НЕ изменился вовсе!
incrementByReference(); // "Внутри (by reference): 1"
std::cout << "Оригинал после by reference: " << counter << std::endl; // 1 — РЕАЛЬНО изменился
return 0;
}
// std::function для хранения лямбд как обычных переменных, передачи и возврата из функций
#include <functional>
#include <iostream>
std::function<int(int, int)> chooseOperation(bool useAddition)
{
if (useAddition) {
return [](int a, int b) { return a + b; }; // лямбда БЕЗ захвата — может конвертироваться даже в указатель на функцию
}
return [](int a, int b) { return a * b; };
}
int main()
{
auto operation = chooseOperation(true);
std::cout << operation(3, 4) << std::endl; // 7
return 0;
}
Пояснения к коду
isAboveThreshold показывает типичное, идиоматичное применение лямбды с захватом по значению — threshold захватывается единожды, на момент создания лямбды, что подходит для случая, когда лямбда передаётся стандартному алгоритму (std::count_if) и значение порога не должно изменяться между вызовами. Сравнение incrementByValue/incrementByReference явно демонстрирует принципиальное различие захвата — несмотря на идентичную логику внутри обеих лямбд (counter++), только версия с захватом по ссылке ([&counter]) реально изменяет оригинальную переменную, видимую за пределами лямбды, тогда как версия с захватом по значению ([counter], дополнительно требующая mutable для возможности изменения захваченной копии внутри тела лямбды) работает исключительно с собственной, изолированной копией, не влияющей на оригинал. std::function<int(int, int)> показывает практичный способ хранения лямбды как обычного значения (возможность вернуть её из функции, сохранить в переменной, передать дальше), что особенно важно, поскольку лямбды без захвата (как в этом примере) технически не имеют единого, простого «типа лямбды» в обычном смысле (каждая лямбда имеет собственный, уникальный, генерируемый компилятором тип), и std::function даёт унифицированную «обёртку», способную хранить любую лямбду (или указатель на функцию) с подходящей сигнатурой.
Подводные камни
- Захват переменной по ссылке (
[&x]) лямбдой, время жизни которой превышает время жизни захваченной переменной — это классический, серьёзный источник неопределённого поведения, особенно когда лямбда сохраняется (например, вstd::functionили асинхронно выполняется позже, в основном цикле статей про асинхронность), а захваченная по ссылке локальная переменная уже вышла из области видимости (статья 525) к моменту реального вызова лямбды, что приводит к обращению к уже несуществующей памяти, аналогично подводным камням про возврат ссылки на локальную переменную. - Использование
[=](захват всех переменных по значению) для лямбды, которая должна изменять одну из захваченных переменных, ожидая, что изменение отразится на оригинале — широкий захват по значению создаёт независимые копии для всех использованных переменных, и без точного, явного захвата конкретной переменной по ссылке ([&specificVariable]) изменения внутри лямбды никогда не повлияют на соответствующие оригинальные переменные за её пределами, что является частой, неочевидной для начинающих ловушкой при первом знакомстве с лямбдами. - Забытый
mutableдля лямбды, захватывающей переменную по значению, но пытающейся изменить эту захваченную копию внутри своего тела — безmutableоператор()лямбды по умолчанию является константным (аналогично const-методу класса), что запрещает изменение захваченных по значению полей внутри тела лямбды, и попытка такого изменения безmutableприводит к ошибке компиляции, явно указывающей на необходимость добавления этого ключевого слова. - Захват
this(неявный или явный, через[this]) в лямбде, переживающей время жизни объекта, к которому принадлежитthis— аналогично общему подводному камню захвата по ссылке, лямбда, захватившаяthisуказатель текущего объекта и сохранённая для использования позже (после потенциального уничтожения этого объекта), при попытке вызова будет обращаться к полям уже не существующего объекта через «висячий»this, что является неопределённым поведением, требующим особого внимания при сохранении лямбд, захватывающихthis, для отложенного, асинхронного выполнения.