Добавить в избранное

Лямбда-выражения в C++: захваты, mutable и практическое применение

Чтение ≈ 5 мин

Лямбды в C++: синтаксис, захват переменных по значению и по ссылке, mutable, generic lambdas, возврат лямбд и где лямбды реально нужны — алгоритмы STL, колбэки, std::thread.

Лямбда — это анонимный объект-функция

Лямбда в 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 строк — вынесите в именованную функцию.
  • Лямбда используется в двух местах — именованная функция или шаблон.
  • Глубокая вложенность лямбд — выносите и называйте, иначе код не читается.

Лямбда — для короткой локальной логики: предикат, колбэк, маленький дженератор. Всё большее — просто функция.

Поделиться
Понравился материал?

Комментарии

Добавить комментарий