Menu

Лямбды в C++: анонимные функции на примерах

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Функции, которые вы пишете там, где используете

На предыдущей странице вы видели, как перегрузка позволяет нескольким функциям делить одно имя. Но иногда именованная функция вообще не нужна - вам требуется крошечный кусочек логики один раз, прямо там, где вы его используете, а присвоение ему имени лишь добавило бы беспорядка. Именно это и есть лямбда: анонимная функция, которую можно определить встроенно.

У лямбды есть характерная форма из четырёх частей:

[capture](parameters) -> return_type { body }

[] - это верный признак того, что перед вами лямбда. Тип возвращаемого значения необязателен - обычно компилятор выводит его сам. Вот простейший возможный вариант:

greet - это просто переменная (её тип неназываем, поэтому вы храните её в auto), которую можно вызвать с помощью (). Лямбды с параметрами работают точно так же, как обычные функции:

Захваты: доступ к окружающей области видимости

То, что делает лямбды чем-то большим, чем просто безымянные функции, - это список захвата, то есть []. Он позволяет лямбде использовать переменные из той области видимости, где она была определена, а не только собственные параметры.

Захватывайте по значению с помощью [x]: лямбда получает собственную копию, зафиксированную в момент создания лямбды.

Обратите внимание, что scale(5) напечатал 50, использовав значение factor, равное 10, которое существовало в момент создания лямбды. Захват по значению делает снимок.

Захватывайте по ссылке с помощью [&x]: лямбда ссылается на исходную переменную, видит последующие изменения и может её менять.

Можно также захватить всё, что использует лямбда, с помощью [=] (всё по значению) или [&] (всё по ссылке). Они удобны, но явная запись - [total] или [&total] - точно документирует, к чему обращается лямбда, и о ней легче рассуждать.

Ловушка висячей ссылки

Захват по ссылке настолько же мощен, насколько и опасен. Ссылка действительна лишь до тех пор, пока жива исходная переменная. Если лямбда переживёт то, что она захватила, вы получите висячую ссылку и неопределённое поведение - программа может рухнуть, может вывести мусор, может случайно показаться работающей.

Вот классическая ошибка: вернуть лямбду, которая захватывает локальную переменную по ссылке.

auto makeCounter() {
    int count = 0;
    return [&count]() { return ++count; };  // ОШИБКА: count умирает здесь
}
// Возвращённая лямбда теперь ссылается на уничтоженную память.

Когда makeCounter возвращает управление, её локальная переменная count уничтожается, но лямбда всё ещё держит ссылку на неё. Вызов возвращённой лямбды обращается к мёртвой памяти. Исправление - захватывать по значению, чтобы лямбда владела собственным состоянием:

Эмпирическое правило: захватывайте по ссылке только тогда, когда лямбда используется немедленно и локально (как с алгоритмами ниже). В тот момент, когда лямбду сохраняют, возвращают или запускают позже, предпочитайте захват по значению.

mutable и типы возвращаемого значения

Заметили mutable в том последнем примере? По умолчанию захват по значению внутри лямбды является const - копию можно читать, но не изменять. Добавление mutable позволяет лямбде менять свои собственные копии между вызовами.

mutable влияет только на приватную копию лямбды - внешний seen остаётся нетронутым, в чём и состоит весь смысл захвата по значению.

В большинстве случаев компилятор прекрасно выводит тип возвращаемого значения. Прописывать его явно через -> нужно лишь при неоднозначности, например для лямбды, которая в разных ветвях могла бы вернуть разные типы:

// Без -> компилятор не может выбрать между int и double
auto half = [](int n) -> double {
    if (n % 2 == 0) return n / 2;   // int
    return n / 2.0;                 // double
};

Лямбды и алгоритмы: настоящая выгода

Причина, по которой лямбды добавили в C++, - это передача коротких кусочков логики алгоритмам стандартной библиотеки. До лямбд приходилось писать отдельную именованную функцию или громоздкий функциональный объект далеко от места его использования. Теперь логика живёт прямо в точке вызова.

Самый частый пример - пользовательский порядок сортировки:

Захваты здесь по-настоящему раскрываются, потому что лямбда может втянуть значение, по которому фильтровать или считать. Это подсчитывает, сколько чисел превышают выбранный пользователем порог:

Поскольку эти лямбды используются немедленно и не переживают окружающую функцию, захват по ссылке ([&passMark]) здесь тоже был бы безопасен - но захват по значению ничуть не менее ясен и никогда не повисает.

Далее: указатели

Лямбды тихо подняли более глубокий вопрос: когда вы захватываете [&x], лямбда держится за местоположение x, а это местоположение остаётся действительным, лишь пока x жив. Именно эта идея - значение, которое ссылается на место, где что-то живёт в памяти, и что происходит, когда то, на что оно указывает, исчезает, - и есть тема следующей страницы. Мы встретимся с указателями лицом к лицу: как взять адрес, как пройти по нему и как та же проблема висячей ссылки, которую вы только что видели, проявляется по всему C++.

Часто задаваемые вопросы

Что такое лямбда в C++?

Лямбда - это анонимная функция, которую можно написать прямо там, где она используется. Синтаксис - [захваты](параметры){ тело }. Она идеальна для коротких, разовых операций - например, для компаратора, который вы передаёте в std::sort, - без необходимости объявлять отдельную именованную функцию где-то ещё.

В чём разница между захватом по значению и по ссылке в лямбде C++?

[x] захватывает копию x, зафиксированную в момент создания лямбды. [&x] захватывает ссылку на исходный x, поэтому лямбда видит последующие изменения и может его менять. Используйте [&] только тогда, когда гарантировано, что захваченные переменные переживут лямбду, иначе вы получите висячую ссылку.

Почему моя лямбда в C++ говорит, что не может изменить захваченную переменную?

Захваты по значению по умолчанию являются const внутри лямбды. Добавьте ключевое слово mutable - [x]() mutable { x++; } -, чтобы лямбда могла изменять свою собственную копию. Учтите, что это меняет только копию лямбды, а не исходную переменную снаружи.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ