С++ для начинающих Указатель this

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

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


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

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

Квадратные скобки представляют собой своеобразную ловушку для захвата внешних по отношению к лямбда-функции переменных. Можно отлавливать всё, что находится в той же области видимости, где расположилась сама лямбда-функция. Есть несколько вариантов отлова: по значению, по ссылке, захват указателя 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,

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

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

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




Все комментарии на сайте проверяются, поэтому ваш комментарий может появиться не сразу. Для вставки кода в комментарий используйте теги: [php]ВАШ_КОД[/php]

6 комментариев: С++ для начинающих Указатель this

  • Татьяна говорит:

    Спасибо. Все очень доступно объяснено.

  • tajkinol говорит:

    Очень хорошее и толковое обьяснение!

  • Lex говорит:

    Благодарю!

  • kolobok говорит:

    Не убедительно и непонятно. Зачем усложнять программу если и так работает:

  • Vasya_RB_007 говорит:

    Уважаемый автор. Предложение из 2-го абзаца не является лаконичным, наверное какое-то слово лишнее.

    Любая функция-элемент класса, не являющаяся статической имеет доступ к объекту, для которого она вызвана, через посредство ключевого слова this.

    1-ый вариант исправления … , через ключевое слово this.

    2-ой вариант исправления …, посредством ключевого слова this.

  • Андрей говорит:

    Автор, код ваш не удобен для копипаста. ЗАмените его на такой

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

Ваш e-mail не будет опубликован.

Поиск

 
     

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

https://www.litres.ru/endru-tanenbaum/operacionnye-sistemy-razrabotka-i-realizaciya-15418253/?lfrom=15589587

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

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