Зачем нужны поля в лямбда-функции?

Ответ

Поля (или захват переменных) позволяют лямбда-функции в C++ получать доступ к данным из окружающей области видимости. Это ключевой механизм для создания замыканий.

Основные способы захвата:

  • По значению [=] или [x]: Создаётся копия переменной на момент создания лямбды. Изменения внутри лямбды не затрагивают оригинал.
  • По ссылке [&] или [&x]: Лямбда работает непосредственно с исходной переменной. Изменения внутри лямбды видны снаружи.
  • Инициализирующий захват (C++14) [x = std::move(obj)]: Позволяет перемещать или инициализировать захватываемые объекты.

Пример:

int counter = 0;
std::string msg = "Hello";

// Захват counter по ссылке, msg по значению
auto lambda = [&counter, msg]() {
    ++counter; // Меняет исходный counter
    std::cout << msg << " called " << counter << " timesn"; // msg - копия
};

lambda(); // Вывод: Hello called 1 times
lambda(); // Вывод: Hello called 2 times
// Исходная msg осталась "Hello"

Критические нюансы:

  1. Время жизни: Захват по ссылке опасен, если лямбда (например, сохранённая в std::function) переживёт захваченную переменную, что приведёт к неопределённому поведению.
  2. По умолчанию const: Лямбда, захватившая переменные по значению, по умолчанию является const. Для модификации таких копий требуется спецификатор mutable.
  3. this: Для захвата членов класса внутри метода используется [this] или [*this] (C++17, захват копии объекта).

Ответ 18+ 🔞

Давай разберём эту тему про захват переменных в лямбдах, а то тут, бля, столько подводных камней, что можно на ровном месте овердохуища проблем нахватать. Смотри, в чём суть.

Представь, что лямбда — это такой хитрожопый зверёк, который живёт внутри твоего кода. И чтобы он мог работать с данными извне, ему нужно их как-то доставить. Вот этот самый процесс доставки и есть захват. А способов доставки — несколько, и каждый со своими, ёпта, последствиями.

Основные способы, как этот зверёк может хапнуть переменные:

  • По значению [=] или, например, [x]: Тут всё просто. Зверёк делает себе личную, отдельную копию переменной. Он с ней там внутри делает что хочет, но на оригинал это нихуя не влияет. Как будто ты сфоткал документ и рисуешь усы на фотке — оригинальный паспорт остаётся чистым.
  • По ссылке [&] или [&x]: А вот это уже интереснее. Зверёк не берёт копию, а просто смотрит на оригинальную переменную, как в окошко. И если он её внутри поменяет — то поменяется она везде. Удобно, но, бля, опасно. Представь, что дом (переменная) снесли, а зверёк всё ещё тычется пальцем в то место, где было окошко. Полный пиздец и неопределённое поведение.
  • Инициализирующий захват (это с C++14 пошло) [x = std::move(obj)]: Это уже для продвинутых. Позволяет не просто скопировать, а, например, переместить ресурс прямо в лямбду, или вообще какую-то хуйню на месте сконструировать. Мощная штука, но надо понимать, что делаешь.

Смотри на примере, как это выглядит в деле:

int counter = 0;
std::string msg = "Hello";

// Захватываем counter по ссылке, а msg — по значению
auto lambda = [&counter, msg]() {
    ++counter; // Меняем исходный counter, потому что взяли на него ссылку
    std::cout << msg << " called " << counter << " timesn"; // msg у нас своя, копия
};

lambda(); // Выведет: Hello called 1 times
lambda(); // Выведет: Hello called 2 times
// Исходная msg так и осталась "Hello", её не тронули

Вроде бы всё логично, да? Но вот где собака, сука, порылась — в нюансах, от которых волосы дыбом встают.

  1. Время жизни — это пиздец какой важный момент. Захватил переменную по ссылке, лямбду куда-то сохранил (в std::function, например), а переменная-то уже померла и накрылась медным тазом. А твоя лямбда потом пытается по этой ссылке обратиться — и получает чистейшее неопределённое поведение. Терпения ноль ебать, когда такие баги ищешь.
  2. Захват по значению — не значит «можно менять». Вот смотри, обычная лямбда, которая что-то захватила по значению — она по умолчанию const. То есть все эти захваченные копии внутри неё — константные. Хочешь их менять? Так объяви лямбду mutable, а то компилятор тебе такое впендюрит, что мало не покажется.
  3. Захват членов класса — отдельная песня. Если ты внутри метода класса пишешь лямбду и хочешь достучаться до полей этого самого класса, то старый способ — это захватить [this]. Лямбда получит указатель и сможет лазить по всем полям объекта. Но опять же, следи за временем жизни объекта! А с C++17 появился вариант получше — [*this]. Это уже захват копии всего объекта по значению. Дороговато может быть, зато безопаснее, если твоя лямбда должна пережить тот экземпляр класса, в котором родилась.

Короче, чувак, инструмент мощный, но, как говорится, вы ходите по охуенно тонкому льду. Один неверный захват по ссылке — и будет вам хиросима с нагасаки в одном флаконе. Думай, что и зачем захватываешь, и всё будет пизда рулю.