Лямбда‑функции в C++: захваты, mutable, возвращаемые типы и лучшие практики
Запрос, под который оптимизирована статья: лямбда‑функции в C++. Если вы хотите быстро и безопасно писать компактные функции прямо в месте вызова — лямбды незаменимы. Разберёмся на практических примерах, без лишней теории.
Базовый синтаксис лямбда‑функций
Общий вид: [](параметры) спецификаторы -> возвращаемый_тип { тело; }. Минимальная лямбда:
#include <iostream>
int main() {
auto hello = [] { std::cout << "Hello, lambda!\n"; };
hello();
}
Захваты: по значению, по ссылке и смешанные
Захват управляет тем, какие внешние переменные доступны внутри лямбды.
- [] — ничего не захватываем
- [=] — по значению (копии) всех используемых переменных
- [&] — по ссылке всех используемых переменных
- [x, &y] — явный смешанный захват
- [this] — захват указателя this (копий нет)
- [*this] (C++23) — копия объекта, на который указывает this
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{5, 1, 4, 2, 3};
int pivot = 3;
// Сортируем так, чтобы числа < pivot были первыми
std::sort(v.begin(), v.end(), [=](int a, int b) {
auto ka = (a < pivot);
auto kb = (b < pivot);
if (ka != kb) return ka > kb; // < перед > — логика группировки
return a < b;
});
for (int x : v) std::cout << x << ' ';
}
Совет: по умолчанию используйте явный захват (например, [pivot]). Глобальные [=] и [&] удобны, но повышают риск случайно захватить лишнее и усложнить сопровождение.
Инициализирующий и перемещающий захват (C++14+)
Можно создавать новые переменные прямо в списке захвата — удобно для перемещения уникальных ресурсов или подготовки состояния.
#include <memory>
#include <thread>
#include <iostream>
int main() {
auto p = std::make_unique<int>(42);
std::thread t([q = std::move(p)]() {
// q - единственный владелец внутри лямбды
std::cout << *q << "\n";
});
t.join();
// p теперь пуст (nullptr)
}
Такой приём безопасен в многопоточных сценариях: вы явно передаёте владение в лямбду.
mutable: изменение копий, а не внешних значений
Лямбды по умолчанию «константные»: захваченные по значению переменные менять нельзя. mutable снимает это ограничение, но меняет только копии.
#include <iostream>
int main() {
int n = 0;
auto inc_copy = [n]() mutable {
n++; // ок, меняем копию
return n;
};
std::cout << inc_copy() << ", " << inc_copy() << "\n"; // 1, 2
std::cout << n << "\n"; // исходный n по-прежнему 0
}
Чтобы менять внешнюю переменную, захватывайте по ссылке: [&n]{ n++; }.
Возвращаемые типы: auto и явное указание
Компилятор выводит тип результата автоматически, но если в ветках возвращаются разные типы, нужна явная аннотация.
auto f = [](bool flag) {
if (flag) return 1; // int
// return 1.5; // ошибка: разные типы
return 2; // ок, везде int
};
auto g = [](bool flag) -> double {
if (flag) return 1; // 1.0
return 1.5; // 1.5
};
Generic‑лямбды (C++14+): параметры как auto
Идеально подходят для алгоритмов стандартной библиотеки и универсальных коллбеков без явного шаблонного синтаксиса.
#include <algorithm>
#include <cctype>
#include <string>
#include <vector>
int main() {
std::vector<std::string> words{"C++", "Lambda", "Rocks"};
std::transform(words.begin(), words.end(), words.begin(), [](auto s) {
for (auto& ch : s) ch = std::toupper(static_cast(ch));
return s;
});
}
Лямбды в алгоритмах: фильтрация и подсчёт
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1,2,3,4,5,6};
v.erase(std::remove_if(v.begin(), v.end(), [](int x){ return x % 2 == 0; }), v.end());
// Остались только нечётные: 1 3 5
for (int x : v) std::cout << x << ' ';
}
std::function и лямбды: когда это оправдано
std::function — тип-обёртка для хранение вызываемого объекта с типовым стиранием. Это гибко, но медленнее и требует выделений. Рекомендации:
- Внутри шаблонов и локально — храните лямбду в
autoили передавайте как универсальный параметр (perfect forwarding). - На границах API (коллбеки, полиморфные контейнеры) — используйте
std::function<...>для стабильного интерфейса.
#include <functional>
#include <iostream>
void run(std::function<void(int)> cb) { cb(42); }
int main() {
auto fast = [](int x){ std::cout << x << "\n"; };
run(fast); // удобно на границе API
}
Статeless‑лямбда как указатель на функцию
Если лямбда ничего не захватывает, её можно неявно преобразовать к указателю на функцию подходящей сигнатуры — удобно для старых C‑API.
void (*fp)(int) = [](int x){ /* ... */ };
fp(10);
Типичные ошибки и как их избежать
- Дэнглинг‑ссылки: вы захватили ссылкой переменную, которая вышла из области видимости. Правило — для асинхронных задач используйте захват по значению или перемещающий захват.
- Глобальные [=] или [&]: скрытые зависимости. Предпочитайте явный захват.
- Тяжёлые копии: большие объекты лучше захватывать по ссылке или по перемещению.
- Сложные типы возвращаемых значений: при неоднородных ветках укажите
-> типявно.
Небольшой чек‑лист по лямбдам
- Пишите очевидный список захвата:
[x],[&y],[z = make()]. - Используйте
mutable, когда меняете копию захваченного значения внутри лямбды. - Предпочитайте
autoдля хранения лямбды, аstd::function— для публичных API. - Для многопоточности — перемещающий захват ресурсов (
[p = std::move(p)]). - Статeless‑лямбды легко передавать туда, где ожидают указатель на функцию.
Куда двигаться дальше
Потренируйтесь переписать свои sort/filter/remove_if на лямбды, попробуйте перемещающий захват и generic‑лямбды. А чтобы системно закрыть пробелы по современному C++ (включая лямбды, ООП, STL и практику), посмотрите программу и первые уроки здесь: C++ с Нуля до Гуру — посмотреть программу и начать бесплатно.
-
Создано 24.12.2025 17:01:46
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.