Урок по треку C++ 5 мин

Лямбды и функциональные объекты

Захваты, mutable, generic-лямбды, std::function и его цена, висячие захваты.

Лямбда-выражение создаёт замыкание: объект безымянного класса с перегруженным operator(). Компилятор генерирует этот класс за вас, захваченные переменные становятся его полями.

int limit = 10;
auto small = [limit](int x) { return x < limit; };
// примерно то же, что:
struct __Lambda {
    int limit;
    bool operator()(int x) const { return x < limit; }
};

Захваты

  • [x] копия x в момент создания лямбды; [&x] ссылка на x.
  • [=] копировать всё используемое, [&] всё по ссылке. Явный список надёжнее: видно, что именно захвачено.
  • [this] захватывает указатель на объект, [*this] (C++17) копию объекта.
  • Инициализирующий захват (C++14): [p = std::move(ptr), n = v.size()]. Так переносят в лямбду move-only объекты вроде unique_ptr.
  • Глобальные и статические переменные не захватываются, они просто доступны.

По умолчанию operator() константный, и менять копии захваченных переменных нельзя. mutable снимает это ограничение; состояние живёт внутри объекта-замыкания, и при копировании лямбды копируется тоже.

С C++20 неявный захват this через [=] объявлен устаревшим: пишите [=, this] явно.

Висячие захваты

Захват по ссылке безопасен, только пока лямбда не переживает переменные.

std::function<int()> make_counter() {
    int count = 0;
    return [&count] { return ++count; };   // count умрёт при выходе: висячая ссылка
}

То же с [this] в колбэке, который вызовут после уничтожения объекта, и с лямбдами, переданными в другой поток. В отложенных вызовах захватывайте по значению или shared_from_this().

Типы лямбд

  • У каждой лямбды уникальный тип, даже у двух одинаковых по тексту.
  • Лямбда без захватов неявно приводится к указателю на функцию: её можно передать в C API.
  • [](auto x) (C++14) generic-лямбда: её operator() шаблонный. В C++20 можно писать явный список параметров шаблона []<class T>(std::vector<T>& v).
  • С C++20 лямбды без захватов конструируются по умолчанию и допустимы в невычисляемом контексте: std::set<int, decltype([](int a, int b) { return a > b; })>.
  • Лямбды по умолчанию constexpr, если тело это позволяет (C++17).

std::function и шаблоны

std::function<R(Args...)> хранит любой вызываемый объект с подходящей сигнатурой: это стирание типа. Цена: возможная аллокация в куче (для больших замыканий, маленькие помещаются в буфер внутри объекта), косвенный вызов, который компилятор обычно не встраивает. Пустой std::function при вызове бросает std::bad_function_call.

Поэтому:

  • если вызываемый объект нужен только внутри функции, принимайте его шаблонным параметром template <class F> void for_each(F f) или std::invocable в C++20: вызов встроится;
  • std::function хорош, когда нужно хранить разнородные колбэки в контейнере или поле класса.

В C++23 появился std::move_only_function для move-only замыканий.

Рекурсивные лямбды

Лямбда не видит собственное имя внутри тела. Варианты: передать её самой себе (auto f = [](auto self, int n) -> int { ... self(self, n - 1) ... }), обернуть в std::function, или в C++23 явный объектный параметр [](this auto self, int n).

Частые ошибки

  • [&] в колбэке, который вызовут позже.
  • Ожидание, что [x] увидит последующие изменения x.
  • Изменение захваченной по значению переменной без mutable (ошибка компиляции) или с mutable, но в копии лямбды.
  • std::function в горячем цикле, где хватило бы шаблона.

Что спросят на собесе

  • Во что компилятор превращает лямбду.
  • Захват по значению и по ссылке, init-capture, mutable.
  • Когда захват по ссылке опасен.
  • Чем std::function отличается от шаблонного параметра и сколько стоит.
  • Как сделать рекурсивную лямбду.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.