лямбда функции c что это
Лямбда-функции в C++
Лямбда-функции появились в C++11. Они представляют собой анонимные функции, которые можно определить в любом месте программы, подходящем по смыслу.
Приведу пример простейшей лямбда функции:
Выражение auto myLambda означает объявление переменной с автоматически определяемым типом. Крайне удобная конструкция C++11, которая позволяет сделать ваш код более лаконичным и устойчивым к изменениям. Настоящий тип лямбда-функции слишком сложный, поэтому набирать его нецелесообразно.
Прежде, чем перейти к более сложным примерам лямбда-функций, определим вспомогательную шаблонную функцию:
Сначала просто выведем переданное лямбда-функции значение:
Теперь рассмотрим возможность «замыкания», т.е. передадим в лямбда-функцию значение локальной переменной по значению:
Но если мы хотим изменить значение переменной внутри лямбда-функции, то можем передать его по ссылке:
Обратите внимание на побочный эффект от связывания переменных с лямбда-функцией по ссылке:
Будьте особенно аккуратны с привязкой параметров по ссылке, когда работаете с циклами. Чтобы не получилось, что все созданные лямбда-функции работали с одним и тем же значением, когда должны иметь собственные копии.
Привязку можно осуществлять по любому числу переменных, комбинируя как передачу по значению, так и по ссылке:
Если требуется привязать сразу все переменные, то можно использовать следующие конструкции:
Допустимо и комбинирование:
Когда использовать лямбда-функции?
Конечно, этот аргумент не обязан быть лямбда-функцией, но часто их применение оказывается оправданным. Например:
Этот код интуитивно понятен, поэтому вы сами с ним легко разберетесь без моих пояснений. Приведу лишь то, что он выведет на консоль:
В своих программах вы тоже можете создавать универсальные функции, для которых управление ходом выполнения осуществляется с помощью функциональных объектов и лямбда-функций в частности.
Синтаксис лямбда-выражений
В этой статье демонстрируется синтаксис и структурные элементы лямбда-выражений. Описание лямбда-выражений см. в разделе лямбда-выражения.
Объекты функций и лямбда-выражения
При написании кода вы, вероятно, используете указатели функций и объекты функций для решения проблем и выполнения вычислений, особенно при использовании алгоритмов стандартной библиотеки C++. У объектов-функций и указателей на функции есть как преимущества, так и недостатки. Например, указатели на функции отличаются минимальными требованиями к синтаксису, но не сохраняют состояние в области видимости. Объекты-функции, в свою очередь, могут сохранять состояние, но требуют дополнительного синтаксиса в определении класса.
Лямбда-выражение сочетает преимущества указателей на функции и объектов-функций, избегая их недостатков. Как и объект функции, лямбда-выражение является гибким и может поддерживать состояние, но в отличие от объекта функции, его синтаксис Compact не требует явного определения класса. С помощью лямбда-выражений можно написать код, который более простым и менее подверженным появлению ошибок, чем код для соответствующего объекта функции.
Пример 1: использование лямбда-выражения
Комментарии
В примере третий аргумент функции for_each является лямбда-выражением. Часть [&evenCount] указывает предложение захвата выражения, (int n) определяет список параметров, а оставшаяся часть определяет тело выражения.
Пример 2: использование объекта-функции
Дополнительные сведения об операторе () см. в разделе вызов функции. Дополнительные сведения о функции for_each см. в разделе for_each.
Лямбда-функции в C++: Нужны или нет?
Под прошлой статьей, посвященной лямбда-функциям в C++ (или лямбда-выражениям) в комментариях было высказано вполне справедливое замечание о том, что заключительный пример вышел не самым наглядным. В связи с этим возникает закономерный вопрос: «А какой пример использования лямбда-функций в C++ можно считать удачным, и существует ли он вообще»? На эти вопросы я и постараюсь ответить.
И начнем с простого примера. Пусть имеется функция-генератор, которая принимает любой вызываемый объект:
Конечно, мы могли бы использовать полиморфизм в стиле ООП. Но это усложнит применение обычных функций. Например, сейчас вполне допустим подобный вызов:
При этом для случая ООП-полиморфного решения понадобится класс-Адаптер для функции. Но это лишний код.
Возможна еще одна ситуация. Пусть в качестве аргумента someGenerator() требуется передать функцию-член. Возникает трудность:
На этом шаге может появиться вопрос: «А не вынести ли функцию processorFunc() в виде отдельного функтора»? Вообще, это могло бы оказаться неплохой идеей. Но на написание функтора всегда уходит больше времени, чем на создание простой функции, которая может нигде и никогда больше не понадобиться.
В программировании есть довольно простое правило: «Писать код прозапас вредно». По смыслу оно напоминает правило о вреде преждевременной оптимизации. Если на данной итерации разработки достаточно простой и более компактной реализации, то на ней и следует остановиться. Принцип KISS в действии.
Здесь я предлагаю сослаться на TDD и гибкие методики разработки ПО в целом. Первое приближение программы должно быть самым простым и кратким. Если понадобится что-то большее, сделаем рефакторинг в будущем. Но не раньше.
В качестве предварительного решения закончим наш класс, воспользовавшись лямбда-функцией следующим образом:
Но это нельзя назвать оптимальным решением. Лямбда-функции известно больше, чем нужно. Инкапсуляция важна. И лучшее ее соблюдать.
Реклама
Чем меньше область видимости, тем лучше
Продолжим работу над предыдущим примером. Реализацию, на которой мы остановились, можно упростить еще больше:
Хорошей практикой программирования является обеспечение минимальной области видимости для переменных. Несколько примеров:
Но лямбда-выражения позволяют объявлять функцию на месте. С минимальной областью видимости. Четко и конкретно по назначению именно там, где она нужна. Конечно, функции во многом отличаются от переменных. Поэтому такая необходимость возникает не часто. Но нельзя исключать такую возможность совсем.
Отсюда следует второй аргумент в пользу лямбда-функций. Если некий простой алгоритм нужен только в одном узком месте, то нет смысла вытаскивать его на поверхность. Достаточно обойтись соответствующей лямбда-функцией. Превратить лямбда-функцию в полноценную функцию или функтор никогда не поздно. Вы легко поймете, если это понадобится, чтобы не нарушать принцип DRY.
Реклама
Выводы
Не могу сказать, что выполнил запланированное в полной мере. Т.е. что привел полноценные примеры и доказательства в защиту лямбда-функций на C++. Подобные рассуждения лучше проводить в процессе разработки. Пройдя несколько итераций. А не на каких-то статичных огрызках кода, которые я могу уместить в рамках статьи.
Примеры лямбда-выражений
В данной статье приводится описание методов использования лямбда-выражений в программах. Общие сведения о лямбда-выражениях см. в разделе лямбда-выражения. Дополнительные сведения о структуре лямбда-выражения см. в разделе синтаксис лямбда-выражения.
Объявление лямбда-выражений
Пример 1
Поскольку лямбда-выражение типизировано, его можно назначить auto переменной или function объекту, как показано ниже:
В примере получается следующий результат.
Remarks
Хотя лямбда-выражения чаще всего объявляются в теле функции, можно объявлять их в любом месте, где можно инициализировать переменные.
Пример 2
Компилятор Microsoft C++ привязывает лямбда-выражение к захваченным переменным при объявлении выражения, а не при вызове выражения. В следующем примере содержится лямбда-выражение, которое фиксирует локальную переменную i по значению, а локальную переменную j — по ссылке. Поскольку лямбда-выражение захватывает i по значению, переопределение i далее в программе не влияет на результат выражения. Однако, поскольку лямбда-выражение захватывает j по ссылке, переопределение j влияет на результат выражения.
В примере получается следующий результат.
Вызов лямбда-выражений
Пример 1
В примере получается следующий результат.
Пример 2
В примере получается следующий результат.
Remarks
Вложенные лямбда-выражения
Пример
Можно вложить одно лямбда-выражение в другое, как показано в следующем примере. Внутреннее лямбда-выражение умножает его аргумент на 2 и возвращает результат. Внешнее лямбда-выражение вызывает внутреннее лямбда-выражение с использованием его аргумента и добавляет к результату 3.
В примере получается следующий результат.
Remarks
В этом примере значение параметра [](int y) < return y * 2; >является вложенным лямбда-выражением.
Higher-Order лямбда-функций
Пример
В примере получается следующий результат.
Использование лямбда-выражения в функции
Пример
Лямбда-выражения можно использовать в теле функции. Лямбда-выражение может получать доступ к любой функции или данным-членам, которые способна использовать включающая функция. Можно явно или неявно захватывать указатель, this чтобы предоставить доступ к функциям и элементам данных включающего класса. Visual Studio 2017 версии 15,3 и более поздних версий (доступно с /std:c++17 ): захват this по значению ( [*this] ), если лямбда-выражение будет использоваться в асинхронных или параллельных операциях, где код может быть выполнен после того, как исходный объект выходит из области действия.
Указатель можно использовать this явно в функции, как показано ниже:
Кроме того, указатель можно захватывать this неявно:
В примере получается следующий результат.
Remarks
Использование лямбда-выражений с шаблонами
Пример
В примере получается следующий результат.
Remarks
Дополнительные сведения о шаблонах C++ см. в разделе шаблоны.
Обработка исключений
Пример
Тело лямбда-выражения выполняет правила как для структурированной обработки исключений (SEH), так и для обработки исключений C++. Можно обработать возникшее исключение в теле лямбда-выражения или перенести обработку исключения во включающий фрагмент. В следующем примере используется for_each функция и лямбда-выражение для заполнения vector объекта значениями другого. Он использует try / catch блок для управления недопустимым доступом к первому вектору.
В примере получается следующий результат.
Remarks
Дополнительные сведения об обработке исключений см. в разделе обработка исключений.
Использование лямбда-выражений с управляемыми типами (C++/CLI)
Пример
Предложение захвата лямбда-выражения не может содержать переменную, которая имеет управляемый тип. Однако можно передать аргумент с управляемым типом в список параметров лямбда-выражения. В следующем примере содержится лямбда-выражение, которое захватывает локальную неуправляемую переменную ch по значению и принимает объект System.String в качестве параметра.
В примере получается следующий результат.
Remarks
Можно также использовать лямбда-выражения с библиотекой STL/CLR. Дополнительные сведения см. в разделе Справочник по библиотеке STL/CLR.
Лямбды: от C++11 до C++20. Часть 2
Привет, хабровчане. В связи со стартом набора в новую группу по курсу «Разработчик C++», делимся с вами переводом второй части статьи «Лямбды: от C++11 до C++20». Первую часть можно прочитать тут.
В первой части серии мы рассмотрели лямбды с точки зрения C++03, C++11 и C++14. В этой статье я описал побуждения, стоящие за этой мощной фичей C++, базовое использование, синтаксис и улучшения в каждом из языковых стандартов. Я также упомянул несколько пограничных случаев.
Теперь пришло время перейти к C++17 и немного заглянуть в будущее (очень близкое!): C++20.
Небольшое напоминание: идея этой серии пришла после одной из наших недавних встреч C++ User Group в Кракове.
У нас был живой сеанс программирования об «истории» лямбда-выражений. Беседу вел эксперт по С++ Томас Каминский (см. профиль Томаса в Linkedin). Вот это событие:
Lambdas: From C++11 to C++20 — C++ User Group Krakow.
Я решил взять код у Томаса (с его разрешения!) и на его основе написать статьи.В первой части серии я рассказывал о лямбда-выражениях следующее:
Теперь давайте посмотрим, что изменилось в C++17 и что мы получим в C++20!
Стандарт (черновик перед публикацией) N659 раздел про лямбды: [expr.prim.lambda]. C++17 привнес два значительных улучшения в лямбда-выражения:
Из expr.prim.lambda #4:
Оператор вызова функции является функцией constexpr, если за объявлением параметра условия соответствующего лямбда-выражения следует constexpr, или он удовлетворяет требованиям для функции constexpr.
Напомним, что в C++17 constexpr функция должна следовать таким правилам:
Поиграться с кодом можно здесь: @Wandbox
Конечно, это еще не все.
Вы можете захватывать переменные (при условии, что они также являются constexpr ):
Но есть интересный случай, когда вы не передаете захваченную переменную дальше, например:
В этом случае в Clang мы можем получить следующее предупреждение:
warning: lambda capture ‘x’ is not required to be captured for this use
Вероятно, это связано с тем, что х можно менять на месте при каждом использовании (если вы не передадите его дальше или не возьмете адрес этого имени).
Но, пожалуйста, сообщите мне, если вы знаете официальные правила для такого поведения. Я нашел только (из cppreference) (но я не могу найти его в черновике. )
(Примечание переводчика: как пишут наши читатели, наверное, имеется в виду подстановка значения ‘x’ в каждом месте, где он используется. Менять его точно нельзя.)
Лямбда-выражение может прочитать значение переменной, не захватывая ее, если переменная
* имеет константный non-volatile целочисленный или перечисляемый тип и была инициализирована с constexpr или
* является constexpr и не имеет изменяемых членов.
Будьте готовы к будущему:
В C++20 у нас будут constexpr стандартные алгоритмы и, возможно, даже некоторые контейнеры, поэтому constexpr лямбды будут очень полезны в этом контексте. Ваш код будет выглядеть одинаково для версии времени выполнения, а также для версии constexpr (версии времени компиляции)!
constexpr лямбда позволяет вам согласовываться с шаблонным программированием и, возможно, иметь более короткий код.
Теперь давайте перейдем ко второй важной фиче, доступной в C++17:
Вы помните нашу проблему, когда мы хотели захватить член класса? По умолчанию мы захватываем this (как указатель!), и поэтому у нас могут возникнуть проблемы, когда временные объекты выходят из области видимости… Это можно исправить, используя метод захвата с инициализатором (см. в первой части серии). Но теперь, в C++17 у нас есть другой путь. Мы можем обернуть копию *this:
Поиграться с кодом можно здесь: @Wandbox
Захват нужной переменной-члена с помощью захвата с инициализатором защищает вас от возможных ошибок с временными значениями, но мы не можем делать то же самое, когда хотим вызвать метод типа:
В C++14 единственный способ сделать код более безопасным захватывать this с инициализатором:
Обратите внимание, что если вы пишете [=] в функции-члене, this захватывается неявно! Это может привести к ошибкам в будущем… и это устареет в C++20.
Вот мы и подошли к следующему разделу: будущее.
В C++20 мы получим следующие функции:
Например, с P1091 вы можете захватить структурированную привязку.
У нас также есть разъяснения, связанные с захватом этого. В C++20 вы получите предупреждение, если захватите [=] в методе:
Существуют также изменения, связанные с расширенными вариантами использования, такими как неоцененные контексты и лямбды без сохранения состояния, которые можно конструировать по умолчанию.
С обоими изменениями вы сможете написать:
Прочитайте мотивы этих функций в первой версии предложений: P0315R0 и P0624R0.
Но давайте посмотрим на одну интересную особенность: лямбда-шаблоны.
В C++14 мы получили обобщенные лямбды, это означает, что параметры, объявленные как auto, являются параметрами шаблона.
Компилятор генерирует оператор вызова, который соответствует следующему шаблонному методу:
Но не было никакого способа изменить этот параметр шаблона и использовать реальные аргументы шаблона. В C++20 это будет возможно.
Например, как мы можем ограничить нашу лямбду, чтобы работать только с векторами некоторого типа?
Мы можем написать общую лямбду:
Но если вы вызовете его с параметром int (например, foo(10); ), вы можете получить какую-то трудночитаемую ошибку:
В С++20 мы можем написать:
Вышеупомянутая лямбда разрешает оператор шаблонного вызова:
Поиграться с кодом можно здесь: @Wandbox
В приведенном выше примере компилятор может предупредить нас о несоответствиях в интерфейсе лямбды, нежели в коде внутри тела.
Другим важным аспектом является то, что в универсальной лямбде у вас есть только переменная, а не ее тип шаблона. Поэтому, если вы хотите получить к нему доступ, вы должны использовать decltype(x) (для лямбда-выражения с аргументом (auto x)). Это делает некоторый код более многословным и сложным.
Например (используя код из P0428):
Теперь можно записать как:
В приведенном выше разделе у нас был краткий обзор C ++ 20, но у меня есть еще один дополнительный пример использования для вас. Эта техника возможна даже в C++14. Так что читайте дальше.
Бонус — LIFTинг с лямбдами
В настоящее время у нас есть проблема, когда у вас есть перегрузки функций, и вы хотите передать их в стандартные алгоритмы (или все, что требует некоторого вызываемого объекта):
Мы получаем следующую ошибку из GCC 9 (trunk):
Однако есть хитрость, в которой мы можем использовать лямбду, а затем вызвать нужную функцию перегрузки.
В базовой форме, для простых типов значений, для наших двух функций мы можем написать следующий код:
И в наиболее общей форме нам нужно немного больше набирать:
Довольно сложный код… верно? 🙂
Давайте попробуем расшифровать его:
Мы создаем обобщенную лямбду и затем передаем все аргументы, которые мы получаем. Чтобы определить его правильно, нам нужно указать noexcept и тип возвращаемого значения. Вот почему мы должны дублировать вызывающий код — чтобы получить правильные типы.
Такой макрос LIFT работает в любом компиляторе, который поддерживает C++14.
Поиграться с кодом можно здесь: @Wandbox
В этом посте мы посмотрели на значительные изменения в C++17, и сделали обзор новых возможностей в C++20.
Можно заметить, что с каждой итерацией языка лямбда-выражения смешиваются с другими элементами C++. Например, до C++17 мы не могли использовать их в контексте constexpr, но теперь это возможно. Аналогично с обобщенными лямбдами начиная с C++14 и их эволюцией в C++20 в форме шаблонных лямбд. Я что-то пропустил? Может быть, у вас есть какой-нибудь захватывающий пример? Пожалуйста, дайте мне знать в комментариях!
Приглашаем всех на традиционный бесплатный вебинар по курсу, который состоится уже завтра 14 июня.