С++. Лямбда-функции. Знакомство с синтаксисом

Что такое лямбда-функции в С++? Я попытаюсь дать вам понять доступное и очень понятное объяснение.

  • Лямбда-функции — это то же, что функциональные_объекты_классов, но имеющие свой собственный синтаксис сущности.
К лямбда-функции можно относиться как к обычной функции, но из-за странного на первый взгляд синтаксиса, коды с этими лямбда-функциями может быть сложно читать. Состоит лямбда-функция из двух основных частей: квадратные скобки и фигурные скобки.


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

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

Квадратные скобки представляют собой своеобразную ловушку для захвата внешних по отношению к лямбда-функции переменных. Можно отлавливать всё, что находится в той же области видимости, где расположилась сама лямбда-функция. Есть несколько вариантов отлова: по значению, по ссылке, захват указателя this по значению, можно захватывать или отдельные переменные, или всё, что есть в зоне видимости.
Если мы захватываем что-то по значению, то внутри лямбда-функции такая захваченная сущность получает себе атрибут "только для чтения", отчего внутри лямбда-функции изменениям захваченное не подлежит:

Захват по ссылке позволяет лямбда-функции напрямую влиять на сущность, и любые изменения внутри лямбда-функции происходят так, как если бы лямбда-функции и не было:

Захватывать можно как по отдельности каждую переменную, так и скопом всё, что есть:

Если вы уберёте захват this в листинге #6.4, То пример просто не скомпилируется. Сам же захват this позволяет легко использовать внутри лямбда-функции все переменные, объявленные в классе:

Надеюсь, примеров достаточно, чтобы понять, что это за захват такой. Захватывать можно только автоматические переменные. Это означает, что нельзя захватывать такие, как глобальные, статические. В примере ниже будет показана попытка захвата по значению, и, как вы помните, захват по значению делает захваченное только для чтения, но не изменения. В нашем же случае изменение произойдёт, потому что никакого захвата не будет.


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

******************

Лямбда-функции в С++ можно запоминать в объекты. Тип у лямбда-функций анонимный, поэтому при указании его используют ключевое слово auto, благодаря которому тип выводится автоматически:

Запомненные лямбда-функции можно использовать как обычные переменные: пересылать их в функции и классы, удобно встраивать в места, где нужны функторы. Если вы разобрались с тем, что описывалось чуть выше, то поймёте листинг #8 без особого труда. Но вы, наверное, обратили внимание, что в одном варианте из двух случаев используется cout, а в другом нет. Это связано с тем, что в том варианте, где используется, возвращается некоторое значение, и лямбда-функция автоматически определяет его тип, в нашем случае тип возвращаемого значения определяется как int, а как нам известно, cout умеет работать c этим типом, поэтому такое вот возможно. В другом же случае нет возвращаемого значения, и тип определяется как void, а с этим товарищем cout Не дружит, вот и всё объяснение. В некоторых случаях компилятор может не определить наиболее предпочтительный тип, и тогда возвращаемый из лямбда-функции тип надо указывать явно:

Надеюсь, и с присваиванием мы разобрались. Может возникнуть вопрос "А как передать объект-хранитель_лямбды в функцию?". Естественно, что если мы имеем объект, то можем его пересылать и принимать.

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

Листинг #9.2 может казаться сложным, но если вы внимательно присмотритесь, то обнаружите, что ничего сложного нет. Для многих из вас, да и для меня, новый элемент немного незнаком, но понять, как использовать его, довольно-таки просто. Используя заголовочный файл functional, мы получаем возможность использовать std::function, которым мы обозначим тип нашей лямбда-функции. Поскольку моя версия лямбда-функции возвращает тип double, то в угловых скобках типа function мы и пишем этот double, а поскольку мы имеем не просто тип, а функцию, то ставим круглые скобки. Поскольку моя версия лямбда-функции имитировала функцию без параметров, то в круглых скобках ничего указывать не нужно. Если бы были параметры, то требовалось бы указать их типы:

Того же эффекта можно добиться с помощью использования шаблонов:

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

Как вы легко можете заметить, x не изменяется, а y изменяется. Связано это с тем, что в первом случае внутри лямбда-функции создаётся локальная копия x, а с y работа происходит с влиянием извнутри лямбда-функции на вовне, поскольку y захвачен по ссылке, то и работает лямбда-функция с ним как если бы обычная функция работала с параметром, принятым по ссылке. Для значения x была создана отдельная временная переменная, в которую и скопировано значение, и при этом эта переменная живёт только внутри лямбда-функции, она локальна по отношению к ней, поэтому внешнее воздействие на x её не затрагивает.
Иногда нужно выполнить захват объекта по значению и иметь возможность изменять значение внутри лямбда-функции. Поскольку по умолчанию мы получаем объект только для чтения, для такого случая нужна подсказка компилятору: необходимо задействовать ключевое слово mutable:

Внутренняя для лямбда-функции x теперь успешно изменяется, но не запутайтесь: внешняя остаётся какой была — поскольку сам весь этот процесс ничем не отличается от работы обычной функции с параметрами по значению. Поскольку лямбда-функция умеет хранить внутреннее состояние, то состояние x сохраняется, пока живёт объект, хранящий лямбда-функцию.
Поскольку лямбда-функции можно запоминать в объекты, объекты-хранители лямбда-функций можно использовать для пересылки в функции, классы, т. е. использовать их как если бы они были обычными переменными. И вот возникает вопрос, а как передать лямбда-функцию в нелямбда-функцию? В некоторых случаях достаточно обойтись функцией, принимающей указатель на функцию, при этом нужно следить, чтобы тип принимаемого указателя соответствовал пересылаемой функции.

Лямбда-функции можно передавать и друг в друга:

Было бы неплохо, если всё было бы так просто с написанием параметров принимающей функции, и параметр ограничивался бы указателем на функцию, но не всегда это прокатывает. Это сработает только при следующих условиях:

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

Можно было бы показать пример попроще, но с ним есть некоторые нюансы:


Листинг #14.2 имеет скрытую проблему. В старых компиляторах этот код может компилироваться, в более новых он компилироваться не будет. Поэтому будем ориентироваться на листинг #14.1. Поскольку есть захват лямбда-функцией, то лямбда-функция к указателю привестись не может, а если она не может привестись к указателю, то и параметр принимающей функции, параметр-указатель, не может подобрать объект-хранитель этой лямбда-функции. Такая вот незадача. Из-за таких ситуаций предпочтение отдаётся не параметрам-указателям_на_функции. В стандартной библиотеке шаблонов существует специальная обёртка, которая позволит обозначить тип для приёма какой угодно функции, в том числе и лямбда-функции. Использование его может показаться запутанным, но если вы хорошо понимаете синтаксис указателей на функции, то проблемы у вас не возникнет. А если вы уже успели понять эту часть, когда я её затронул, то в тем более.

Немного объясню о синтаксисе std::function. В угловых скобках обозначается тип принимаемой функции. Поскольку тип нашей лямбда-функции соответствовал типу void, то на этот раз в угловых скобках тип обозначился void. Если бы тип результирующего объекта лямбда-функции был другим, то вместо этого void указывался бы он. А поскольку это всё-таки функция, к этому типу добавляется список формальных параметров. Но о параметрах чуть позднее.

Остаётся надеяться, что стало понятно, что именно подаётся в угловые скобки. Рядом с угловыми скобками используются круглые, обозначающие список формальных параметров принимаемой функции. Поскольку в этот раз у нас использовалась лямбда-функция, соответствующая функции без параметров, круглые скобки оставались пустыми. Если бы лямбда-функция соответствовала функции с параметрами, то типы тех параметров были бы описаны в круглых скобках:

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


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

  • Замыкание (closure) — результирующий объект, создаваемый лямбда-функцией.

  • Класс замыкания (closure class) — класс, из которого инстанцируется замыкание.

  • Лямбда-интродьюсер — Те скобки, которые предназначены для захвата, со всем своим содержимым.

  • trailing-return-type — возвращаемый из лямбды явно тип для создаваемого лямбдой конечного объекта

  • capture-list — список захватываемых объектов. В него входят все те значения, которые захватываются явно. Общее обозначение всего того, что не &, =, this,

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

  • Во время захвата явно перечисляйте захватываемые локальные переменные.

Вы когда захватываете всё скопом, используя [&] или [=], то можете упустить из виду некоторые моменты (или по незнанию, или от невнимательности), и потом, если случится так, что некоторый объект с лямбда-функцией переживёт собственную лямбда-функцию, то дальнейшие обращения к объекту будут вести к непредсказуемому поведению программы.
Небольшая шпаргалка по способам захвата:

Есть ещё один такой скользоватый момент. Например, если мы используем захват по умолчанию, но не используем внутри лямбды некоторые переменные, потенциально готовые к использованию, то такие неиспользованные переменные считаются как незахваченные:

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

Такая вот существует тонкость захвата.
Разумеется, есть ещё и другие моменты, касаемые лямбда-функций, но я, пожалуй, остановлюсь на этом. Я бы их описал, но я не могу учесть всех аспектов. Если вы освоили что-нибудь нужное, то это здорово.

5 комментариев на «“С++. Лямбда-функции. Знакомство с синтаксисом”»

  1. Томов:

    Спасибо автору! Написанное очень помогло!

  2. Владимир:

    Увидеть бы пример создания массива указателей на Лямбда функции. Статья классная.

    • Само собой, массив указателей на функции предполагает, как и любой массив, что в хранении будут использоваться однотипные данные или ссылки на однотипные данные, т. е. сейчас лямбда-функции в массиве все должны быть с одинаковой сигнатурой. А так это обычный масив указателей на функции.

      Для лямбд с захватом надо использовать std::function, использовать обычные указатели на лямбды с захватом не получится.

      • Владимир:

        проверил работает? В квадратных скобочках можно передавать параметры для захвата контекста как угодно а в аргументах лямбды как и сказал админ всё должно совпадать. А здесь не приходят уведомления на эл. почту если вам отвечают?

        • Мне? Приходят. Незарегистрированным, наверное, нет, не знаю. Регистрацию я вроде убрал. Есть некоторые технические сложности и моё незнание, мешающие исправить это. Впрочем, вернул регистрацию. Но вход может плохо работать: в смысле, что при правильных входных авторизации пишет будто неверный логин или пароль (в теме сайта устаревший код, из-за чего такая фигня).

          Сообщение написано непонятно: Вы что, себя спрашиваете и себе отвечаете?

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/skott-mayers/effektivnoe-ispolzovanie-c-55-vernyh-sposobov-uluchshit-strukturu-i-kod-vashih-programm/?lfrom=15589587

Последние комментарии

Яндекс.Метрика