Зачем вообще нужны лямбды
Начнём с задачи, где лямбда решает проблему, а без неё приходится писать много лишнего кода.
Задача: отсортировать вектор чисел по убыванию модуля.
Функция (C-style)
bool compareAbs(int a, int b) {
return abs(a) > abs(b);
}
sort(v.begin(), v.end(),
compareAbs);
Функтор (класс)
struct CompareAbs {
bool operator()(int a, int b) const {
return abs(a) > abs(b);
}
};
sort(v.begin(), v.end(),
CompareAbs{});
Лямбда (C++11)
sort(v.begin(), v.end(),
[](int a, int b) {
return abs(a) > abs(b);
});
Анатомия лямбда-выражения
Разберём синтаксис по частям. У лямбды есть пять компонент, из которых обязательны только две.
// Пустой список захвата — ведёт себя как обычная функция
auto square = [](int x) { return x * x; };
cout << square(5); // 25
// То же самое, что написать функцию:
int square(int x) { return x * x; }
operator(),
но нам не нужно об этом думать.
Захват переменных: главный источник путаницы
Лямбда может использовать переменные из внешней области видимости — но только те, которые явно указаны в списке захвата. Способ захвата влияет на поведение: копия или ссылка?
| Синтаксис | Что захватывает | Что происходит при изменении внешней переменной |
|---|---|---|
[] |
Ничего | Переменные недоступны внутри лямбды |
[x] |
Копию x |
Изменения внешнего x не видны |
[&x] |
Ссылку на x |
Изменения внешнего x видны сразу |
[=] |
Копии всех используемых переменных | Изменения внешних переменных не видны |
[&] |
Ссылки на все используемые переменные | Изменения внешних переменных видны |
[=, &x] |
Копии всех, кроме x (он по ссылке) |
Гибридный вариант |
[&, x] |
Ссылки на все, кроме x (он по копии) |
Гибридный вариант |
Демонстрация 1: что на самом деле захватывает лямбда
Слева — «внешние» переменные, справа — что увидят лямбды. Измените значение и посмотрите, как поведут себя разные захваты.
Три лямбды в программе
auto byCopy = [x]() {
return x;
};
auto byRef = [&x]() {
return x;
};
auto byCopyAll = [=]() {
return x;
};
Что видят лямбды
Замыкания: почему это работает
Когда лямбда захватывает переменные, компилятор создаёт объект-замыкание —
класс со скрытыми полями для каждой захваченной переменной и одним методом
operator(). Именно поэтому захваченные переменные «живут» внутри
лямбды.
Это лямбда:
int x = 10, y = 20;
auto sum = [x, y](int z) { return x + y + z; };
cout << sum(5); // 35
Это то, во что её превращает компилятор:
class __lambda_1 {
int x; // копия внешней x
int y; // копия внешней y
public:
__lambda_1(int _x, int _y) : x(_x), y(_y) {}
int operator()(int z) const {
return x + y + z;
}
};
auto sum = __lambda_1(x, y); // захват = вызов конструктора
cout << sum(5); // вызов operator()
operator() помечен const. По умолчанию
лямбда не может менять захваченные по значению переменные — её operator()
константный. Чтобы разрешить изменения, нужно ключевое слово mutable.
Ключевое слово mutable
Оно снимает константность с operator(), позволяя лямбде менять свои
копии захваченных переменных. Важно: речь идёт именно о копиях —
внешние переменные при этом не меняются.
Демонстрация 2: как работает mutable
Лямбда со счётчиком вызовов. Нажмите «Вызвать лямбду» несколько раз и следите за состоянием. Обратите внимание: внешняя переменная не меняется.
Код
int counter = 0;
auto next = [counter]() mutable {
counter++;
return counter;
};
// Первый вызов → 1
// Второй вызов → 2
// Третий вызов → 3
// Внешний counter всё ещё 0!
Состояние
Главная ловушка: висячие ссылки в лямбдах
Захват по ссылке опасен, когда лямбда живёт дольше, чем захваченная переменная. Компилятор об этом не предупредит — программа просто начнёт выдавать мусор.
Демонстрация 3: что происходит при висячей ссылке
Программа создаёт лямбду внутри блока, возвращает её наружу, а затем вызывает. Пройдите по шагам и увидите, что при захвате по ссылке лямбда «читает мусор».
Код программы
Состояние памяти
std::shared_ptr.
std::function: универсальная обёртка для вызываемых объектов
Лямбда имеет уникальный тип, известный только компилятору. Если нужно сохранить
её в переменной, которая объявлена в другом месте (в заголовке класса, в контейнере,
в параметре функции), используется std::function.
#include <functional>
// Лямбда без захвата — можно сохранить в указатель на функцию
int (*fp)(int) = [](int x) { return x * x; };
// Лямбда с захватом — только в std::function
int threshold = 5;
function<bool(int)> isBig = [threshold](int x) {
return x > threshold;
};
// В std::function можно положить:
// 1. Лямбду со захватом
// 2. Обычную функцию
// 3. Функтор
// 4. Указатель на функцию-член
isBig(10); // true
isBig(3); // false
std::function. Она стирает тип — отсюда небольшие
накладные расходы (виртуальный вызов + возможно копирование лямбды в внутренний буфер).
Если лямбда используется только локально, лучше оставить auto —
это быстрее и позволяет компилятору инлайнить.
Демонстрация 4: лямбды в конвейере обработки
Пять лямбд сохранены в векторе std::function. Каждая получает число
и возвращает результат. Прогоните конвейер по шагам.
#include <vector>
#include <functional>
using namespace std;
int main() {
vector<function<int(int)>> pipeline;
pipeline.push_back([](int x) { return x + 10; });
pipeline.push_back([](int x) { return x * 2; });
pipeline.push_back([](int x) { return abs(x); });
pipeline.push_back([](int x) { return x - 5; });
int value = 2;
for (auto& step : pipeline) {
value = step(value);
}
cout << value << endl;
}
Универсальные лямбды и другие возможности C++14
auto в параметрах — шаблонная лямбда
auto print = [](const auto& x) {
cout << x << endl;
};
print(42); // int
print(3.14); // double
print(string("hi")); // std::string
Инициализация захвата (init capture)
int x = 10;
// Новая переменная y живёт в лямбде
auto f = [y = x * 2]() { return y; };
cout << f(); // 20
// Можно переместить объект в лямбду
auto g = [data = std::move(vect)]() {
// data — теперь собственность лямбды
};
IIFE — лямбда, вызываемая сразу
Иногда нужно выполнить код один раз для инициализации. Лямбда позволяет сделать это, не создавая отдельную функцию.
const int config_value = []() {
if (some_condition)
return 42;
else
return 17;
}(); // ← обратите внимание на скобки в конце
// Значение получено и присвоено сразу
// Никакой отдельной функции не нужно
Лямбды в алгоритмах STL
Это самое частое применение лямбд. Стандартные алгоритмы принимают функцию — и лямбда позволяет описать эту функцию прямо на месте.
vector<int> v = {3, 5, 7, 8, 11, 12};
auto it = find_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; });
if (it != v.end())
cout << *it; // 8
struct Person { string name; int age; };
vector<Person> people = { /* ... */ };
sort(people.begin(), people.end(),
[](const Person& a, const Person& b) {
return a.age < b.age;
});
for_each(v.begin(), v.end(),
[](int x) { cout << x * 2 << " "; });
// Похоже на range-for, но позволяет
// использовать std::function,
// прерывать по условию и т.д.
int threshold = 10;
int count = count_if(v.begin(), v.end(),
[threshold](int x) {
return x > threshold;
});
Лучшие практики
1. Предпочитайте явные захваты [x, &y]
[=] и [&] удобны, но опасны: [=] копирует
больше, чем нужно, а [&] может оставить висячие ссылки. Явный
список захвата делает намерения программиста прозрачными.
2. Для лямбд «на потом» — только захват по значению
Если лямбда сохраняется в контейнер, передаётся в другой поток или возвращается
из функции — захват по ссылке почти наверняка создаст висячую ссылку. Захватывайте
по значению или используйте shared_ptr.
3. Не используйте std::function там, где хватит auto
auto f = [](int x) { ... }; хранит лямбду в её собственном типе —
компилятор может инлайнить вызов. std::function добавляет виртуальный
вызов. Используйте его, только когда нужно хранить разные вызываемые объекты
под одним типом.
4. mutable — только когда действительно нужен
Лямбда, меняющая своё состояние между вызовами — редкая ситуация. Если она
используется в алгоритмах STL (вроде for_each), поведение может
стать непредсказуемым: неизвестно, в каком порядке алгоритм будет обходить элементы.
Антипаттерн: захват [&] в колбэке, который переживёт функцию
function<void()> makeCallback() {
int local = 42;
return [&local]() { // ← захват по ссылке
cout << local << endl;
};
}
int main() {
auto cb = makeCallback();
cb(); // читает мусор — local уже не существует
}
Решение: [local]() { ... } — захват по значению.
Антипаттерн: захват [=] от this в классах
В C++11/14 захват [=] в функции-члене захватывает указатель
this, а не копию объекта. Если объект будет уничтожен, а лямбда —
сохранена, получите висячий указатель на this. В C++17 появился
явный захват [*this] — он действительно копирует объект.
Шпаргалка
| Синтаксис | Значение |
|---|---|
[](){ } | Лямбда без захвата — эквивалент обычной функции |
[x](){ } | Захват копии x |
[&x](){ } | Захват ссылки на x |
[=](){ } | Копии всех используемых переменных |
[&](){ } | Ссылки на все используемые переменные |
[x, &y](){ } | Комбинация: копия x, ссылка на y |
[y = x*2](){ } | Init capture: создаём новую переменную y |
[ptr = std::move(v)](){ } | Захват через перемещение |
[](int x) mutable { } | Разрешить менять копии захваченных переменных |
[](auto x){ } | Универсальная лямбда — работает с любым типом |
[](){ ... }() | IIFE: немедленно вызвать лямбду |
function<int(int)> f = [](int x){ return x; }; | Сохранить лямбду в std::function |
Задание
- Напишите лямбду
[](int a, int b){ return a + b; }и вызовите её. Чем она отличается от обычной функции? - Внутри функции создайте
int x = 10;. Определите две лямбды:[x]и[&x], возвращающиеx. Изменитеx = 100и вызовите обе. Объясните разницу. - Напишите лямбду со счётчиком вызовов через
mutable. Сколько раз нужно вызвать, чтобы счётчик дошёл до 5? - Объясните, почему этот код опасен:
auto make = []() { int x = 42; return [&x]{ return x; }; }; auto f = make(); cout << f(); - Реализуйте ту же функцию, но безопасно (через захват по значению).
- Отсортируйте вектор строк по длине, затем по алфавиту при равной длине. Используйте лямбду как компаратор.
- Создайте
std::function<int(int)>и сохраните в него лямбду[](int x){ return x * x; }. Вызовите её дважды. - Захватите уникальный указатель в лямбду через перемещение:
Почему после этогоauto ptr = make_unique<int>(42); auto f = [p = std::move(ptr)]() { return *p; };ptrнельзя использовать? - Реализуйте IIFE, который возвращает разные значения в зависимости от условия. В чём преимущество по сравнению с отдельной функцией?
- Используйте
std::for_eachс лямбдой, чтобы увеличить каждый элемент вектора в два раза. Обратите внимание: чтобы изменить элемент, лямбда должна принимать его по ссылке.