Лямбда — это анонимный объект-функция
Лямбда в C++ создаёт объект с operator() — «функтор», который пишется прямо в месте использования. Вместо описания отдельной функции или структуры-функтора для одной строки логики вы пишете её на месте:
auto square = [](int x) { return x * x; };
square(5); // 25
std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // по убыванию
Общий синтаксис: [захваты](параметры) -> тип_возврата { тело }. Хвостовой тип возврата часто опускают — компилятор выведет из return.
Захваты: что лямбда видит снаружи
Квадратные скобки — это список захвата: какие переменные из окружающего скоупа лямбда увидит и как. Это главное отличие от обычной функции и главный источник ошибок.
int threshold = 10;
std::string prefix = "log: ";
auto f1 = [threshold](int x) { return x > threshold; }; // по значению — копия
auto f2 = [&prefix]() { prefix += "!"; }; // по ссылке — меняет оригинал
auto f3 = [=]() { return threshold + 1; }; // все используемые — по значению
auto f4 = [&]() { /* видит всё по ссылке */ };
auto f5 = [threshold, &prefix]() {}; // смешанный захват
auto f6 = [=, &prefix]() {}; // всё по значению, prefix по ссылке
По значению vs по ссылке — когда что
- По значению
[=]: копия на момент создания лямбды. Безопасно, если лямбда переживёт окружающий скоуп (колбэки, потоки). - По ссылке
[&]: лямбда ссылается на внешнюю переменную. Опасно при асинхронности — переменная умрёт, ссылка зависнет.
std::function<void()> dangerous() {
int counter = 0;
return [&counter]() { ++counter; }; // ЛОВУШКА: counter умрёт при выходе
}
std::function<void()> safe() {
int counter = 0;
return [counter]() mutable { ++counter; }; // копия — безопасно
}
mutable и захват по перемещению
По умолчанию operator() лямбды константный — захваченные копии нельзя менять. mutable снимает это ограничение (копия, не оригинал!):
int n = 0;
auto gen = [n]() mutable { return ++n; }; // у каждой копии лямбды свой n
gen(); // 1
gen(); // 2 — n внутри лямбды растёт, снаружи остался 0
С C++14 можно захватывать с инициализацией — в том числе перемещать в лямбду unique_ptr:
auto ptr = std::make_unique<Connection>(args);
auto task = [p = std::move(ptr)]() { // захват по перемещению
p->send();
};
Generic lambdas и возвращаемый тип
С C++14 параметры могут быть auto — лямбда превращается в шаблон:
auto print = [](const auto& x) { std::cout << x << "\n"; };
print(42); print(std::string("hi")); print(3.14);
// C++20 — явные шаблонные параметры
auto typed = []<typename T>(T a, T b) { return a + b; };
Тип возврата выводится из return, но если return несколько и возвращают разное — укажите явно:
auto f = [](bool flag) -> double {
if (flag) return 1; // int
return 3.14; // double — без -> double не скомпилируется... точнее скомпилируется с ошибкой неоднозначности
};
Где лямбды реально полезны
Алгоритмы STL — самое частое применение
// найти первый элемент по условию
auto it = std::find_if(v.begin(), v.end(),
[](const User& u) { return u.age > 18 && u.active; });
// трансформация
std::transform(v.begin(), v.end(), out.begin(),
[](const User& u) { return u.name; });
// подсчёт
auto adults = std::count_if(v.begin(), v.end(),
[](const User& u) { return u.age >= 18; });
// накопление с состоянием
double total = 0;
std::for_each(v.begin(), v.end(),
[&total](const Order& o) { total += o.sum; });
Колбэки и асинхронность
button.onClick([this]() { handleClick(); });
std::thread worker([shared = sharedState] {
shared->process();
});
worker.detach();
В колбэках захват this/shared_from_this критичен: лямбда может вызваться после смерти объекта. Для shared-объектов захватывают std::weak_ptr или shared_from_this — иначе висячий this.
Локальные «функции»
Лямбда заменяет маленькую функцию, нужную один раз:
auto trim = [](std::string s) {
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notSpace));
s.erase(std::find_if(s.rbegin(), s.rend(), notSpace).base(), s.end());
return s;
};
std::function vs auto для хранения лямбды
auto f = [](int x) { return x; }; // точный тип, ноль оверхеда, имя типа недоступно
std::function<int(int)> g = f; // type-erasure обёртка, есть небольшой оверхед, но единый тип
Правило: внутри функции — auto; если лямбду надо хранить в поле класса, передавать через интерфейс или держать коллекцию разнородных — std::function.
Типичные ошибки
[&]и уход из скоупа — висячие ссылки, самая частая беда при потоках и отложенных вызовах.thisв долгоживущих лямбдах без проверки времени жизни объекта.mutableбез понимания: меняется копия, внешняя переменная не трогается.- Захват по значению тяжёлого объекта случайно:
[data]гдеdata— мегабайтный вектор, при каждом создании лямбды копия. - Глубокая вложенность лямбд делает код нечитаемым — вынесите в именованную функцию.
Итог
Лямбды — стандартный способ передавать поведение: предикаты в алгоритмы, колбэки, обработчики. Освойте четыре вещи: [=]/[&] и смешанный захват, mutable для изменения копий, auto-параметры для generic-кода, осторожность с this и временем жизни захваченных ссылок.
Лямбда как поле класса и функция
Лямбды можно хранить для отложенного вызова:
class Button {
std::function<void()> onClick_;
public:
void onClick(std::function<void()> f) { onClick_ = std::move(f); }
void click() { if (onClick_) onClick_(); }
};
btn.onClick([this] { save(); refresh(); });
Лямбды в constexpr и шаблонах (C++20)
С C++20 лямбда может быть constexpr и даже использоваться в метапрограммировании:
constexpr auto inc = [](int x) { return x + 1; };
static_assert(inc(4) == 5);
auto twice = []<typename T>(T v) { return v + v; }; // шаблонная лямбда
Захват this по значению (C++17)
[this] захватывает указатель — опасно при долгоживущих лямбдах. [*this] — копирует объект целиком в лямбду:
auto f = [*this]() { return x_ + y_; }; // копия объекта — живёт в лямбде
Для shared-объектов — [self = shared_from_this()] держит объект живым, пока жива лямбда.
Мини-проект: система обратных вызовов
Напишите класс EventEmitter с on(event, лямбда) и emit(event) — карта unordered_map<string, vector<function<void()>>>. Это паттерн, на котором построены интерфейсы, игры и IoT-обработчики — и отличное упражнение на захваты и std::function.
Когда лямбда не нужна
- Логика сложнее 5–10 строк — вынесите в именованную функцию.
- Лямбда используется в двух местах — именованная функция или шаблон.
- Глубокая вложенность лямбд — выносите и называйте, иначе код не читается.
Лямбда — для короткой локальной логики: предикат, колбэк, маленький дженератор. Всё большее — просто функция.