Использование лямбда функций c

Опубликовано: 18.05.2024

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

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

Возможные варианты синтаксиса лямбда функций

Первый вариант является полным, но не запрещается использовать сокращённые вариации записи функций.

  • capture - список внешних захватываемых объектов, они могут захватываться как по ссылке, так и копированием.
  • params - список параметров, передаваемых в лямбда функции, данная часть будет аналогична записи аргументов для обычных функций.
  • mutable - использование mutable позволяет модифицировать копии объектов, которые были захвачены копированием. В обычном варианте они не будут модифицироваться.
  • exception - обеспечивает спецификацию исключения, то есть лямбда функции также как и обычные функции могут выкидывать исключения.
  • attribute - обеспечивает спецификацию атрибута, таких атрибутов в спецификации C++ определено всего два ([[noreturn]], [[carries_dependency]])
    • params - список параметров, передаваемых в лямбда функцию
    • ret - возвращаемое значение лямбда функции

    Что касается возвращаемого значение, то оно может автоматически выводиться из типа объекта, который возвращается оператором return. Если же в лямбда-функции отсутствует оператор return, то возвращаемое значение будет void.

    Лямбда функция создаёт безымянный временный объект уникального безымянного non-union, non-aggregate типа, известного как тип замыкания. Благодаря введению оператора auto в современном стандарте C++ можно объявить объект лямбда функции довольно легко, без прописывания объявления функтора ( std::function ) со всеми апраметрами и возвращаемыми значениями, что делает код более простым и читаемым (для опытного программиста, конечно. Безусловно нужно учитывать то, что новичок быстрее заподозрит неладное, если в объявлении лямбды будет фигурировать std::function, но это уже вопрос практики).

    Вот пример объявления простой лямбда функции, которая будет возвращать тип void , поскольку отсутствует хотя бы один оператор return .

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

    Вот пример кода, который не скомпилируется.

    Нужно указать тип возвращаемого значения

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

    Опять нужно указать тип возвращаемого значения

    Дело в том, что nullptr - это универсальный тип данных, который в каком-то смысле не является типом данных, поскольку его нельзя установить в качестве типа переменной. Но он может быть присвоен в качестве значения указателю на объект. Чтобы неявное преобразование в данном случае происходило правильно, нужно также указать тип возвращаемого значения.

    Также в выше приведённом примере показано, как вызвать лямда функцию и передать в неё параметры. Заметили? В данном примере используется параметр int type , в зависимости от которого мы возвращаем указатель на созданный объект или nullptr .

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

    Список символов может быть передан следующим образом:

    • [a,&b] где a захвачена по значению, а b захвачена по ссылке.
    • [this] захватывает указатель this по значению.
    • [&] захват всех символов по ссылке
    • [=] захват всех символов по значению
    • [] ничего не захватывает

    Про захват переменных поговорим в следующих статьях.

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

    Лямбда-функции появились в C++11. Они представляют собой анонимные функции, которые можно определить в любом месте программы, подходящем по смыслу.

    Приведу пример простейшей лямбда функции:

    Выражение auto myLambda означает объявление переменной с автоматически определяемым типом. Крайне удобная конструкция C++11, которая позволяет сделать ваш код более лаконичным и устойчивым к изменениям. Настоящий тип лямбда-функции слишком сложный, поэтому набирать его нецелесообразно.

    Непосредственное объявление лямбда-функции []() < std::cout << "Hello, lambda!" << std::endl; >состоит из трех частей. Первая часть (квадратные скобки [] ) позволяет привязывать переменные, доступные в текущей области видимости. Вторая часть (круглые скобки () ) указывает список принимаемых параметров лямбда-функции. Третья часть (в фигурных скобках <> ) содержит тело лямбда-функции.

    Вызов определенной лямбда-функции ничем не отличается от вызова обычной функции: myLambda() . В нашем случае на консоль будет выведено сообщение:

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

    В качестве аргумента она принимает любой объект, который можно вызвать с аргументом -5 . Более подробно о создании таких функций мы говорили, когда рассматривали указатели на функции в C++. Мы будем передавать в call() наши лямбда-функции для запуска.

    Сначала просто выведем переданное лямбда-функции значение:

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

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

    Обратите внимание на побочный эффект от связывания переменных с лямбда-функцией по ссылке:

    Будьте особенно аккуратны с привязкой параметров по ссылке, когда работаете с циклами. Чтобы не получилось, что все созданные лямбда-функции работали с одним и тем же значением, когда должны иметь собственные копии.

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

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

    Допустимо и комбинирование:

    Однако замечу, что на практике лучше не использовать обобщенное привязывание через = и & , а явно обозначать необходимые переменные по одной. Иначе могут возникнуть загадочные ошибки из-за конфликтов имен.

    Когда использовать лямбда-функции?

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

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

    Этот код интуитивно понятен, поэтому вы сами с ним легко разберетесь без моих пояснений. Приведу лишь то, что он выведет на консоль:

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

    В C++ 11 и более поздних версиях лямбда-выражение, часто называемое лямбда– — это удобный способ определения объекта анонимной функции ( замыкания) непосредственно в расположении, где оно вызывается или передается в качестве аргумента функции. Обычно лямбда-выражения используются для инкапсуляции нескольких строк кода, передаваемых алгоритмам или асинхронным методам. В этой статье приводится определение лямбда-выражений, их сравнение с другими методами программирования, описание их преимуществ и простой пример.

    См. также

    Структура лямбда-выражения

    В стандарте ISO C++ демонстрируется простое лямбда-выражение, передаваемое функции std::sort() в качестве третьего аргумента:

    На следующем рисунке показана структура лямбда-выражения:

    предложение Capture (также известное как оператор лямбда-выражения в спецификации C++).

    список параметров Используемых. (Также называется лямбда-объявлением)

    изменяемая спецификация Используемых.

    Спецификация Exception Используемых.

    замыкающий-возвращаемый тип Используемых.

    Предложение фиксации

    Лямбда-выражение может добавлять новые переменные в тексте (в C++ 14), а также получать доступ к переменным из окружающей области или записывать их. Лямбда-выражение начинается с предложения Capture (лямбда- знаком в стандартном синтаксисе), который указывает, какие переменные захватываются и является ли захват значением или ссылкой. Доступ к переменным с префиксом с амперсандом ( & ) осуществляется по ссылке, а к переменным без префикса — по значению.

    Пустое предложение фиксации ( [ ] ) показывает, что тело лямбда-выражения не осуществляет доступ к переменным во внешней области видимости.

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

    При использовании записи по умолчанию фиксируются только переменные, указанные в лямбда-выражении.

    Если предложение Capture включает запись по умолчанию & , то identifier в capture предложении записи нет возможности использовать форму & identifier . Аналогично, если предложение Capture включает запись-Default = , то ни одно capture из этого предложения записи не может иметь форму = identifier . Идентификатор или this не может использоваться в предложении Capture более одного раза. В следующем фрагменте кода приводится несколько примеров.

    Захват, за которым следует многоточие, — это расширение пакета, как показано в следующем примере шаблона Variadic :

    Чтобы использовать лямбда-выражения в теле метода класса, передайте this указатель в предложение Capture, чтобы предоставить доступ к методам и членам данных включающего класса.

    Visual Studio 2017 версии 15,3 и более поздних версий (доступно с /std: c++ 17): this указатель может быть захвачен значением путем указания *this в предложении Capture. Захват по значению означает, что весь замыкание, которое является объектом анонимной функции, енкапулатес лямбда-выражение, копируется в каждый сайт вызова, где вызывается лямбда. Захват по значению полезен, когда лямбда-выражение будет выполняться в параллельных или асинхронных операциях, особенно на определенных аппаратных архитектурах, таких как NUMA.

    Пример, демонстрирующий использование лямбда-выражений с методами класса, см. в разделе "пример: использование лямбда-выражения в методе" в примерах лямбда-выражений.

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

    Фиксацию ссылок можно использовать для изменения переменных снаружи, тогда как фиксацию значений нельзя. ( mutable позволяет изменять копии, но не оригиналы.)

    Фиксация ссылок отражает изменение переменных снаружи, тогда как фиксация значений — нет.

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

    Обобщенная фиксация (C++14)

    В C++14 вы можете объявлять и инициализировать новые переменные в предложении фиксации. Для этого не требуется, чтобы эти переменные существовали во внешней области видимости лямбда-функции. Инициализация может быть выражена в качестве любого произвольного выражения. Тип новой переменной определяется типом, который создается выражением. Одно из преимуществ этой возможности заключается в том, что в C++14 таким образом можно фиксировать переменные из окружающей области видимости, доступные только для перемещения (например std::unique_ptr), и использовать их в лямбда-выражении.

    Список параметров

    В дополнение к возможности фиксации переменных, лямбда-выражения могут принимать входные параметры. Список параметров (лямбда-декларатор в стандартном синтаксисе) является необязательным и в большинстве аспектов напоминает список параметров для функции.

    В C++ 14, если тип параметра является универсальным, можно использовать auto ключевое слово в качестве спецификатора типа. Это отдает компилятору команду создать оператор вызова функции в качестве шаблона. Каждый экземпляр auto в списке параметров эквивалентен отдельному параметру типа.

    Лямбда-выражение может принимать другое лямбда-выражение в качестве своего аргумента. Дополнительные сведения см. в разделе "лямбда-выражения более высокого порядка" статьи Примеры лямбда-выражений.

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

    Отключаемая спецификация

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

    Спецификация исключений

    Можно использовать noexcept спецификацию исключения, чтобы указать, что лямбда-выражение не создает исключений. Как и в случае с обычными функциями, компилятор Microsoft C++ создает предупреждение C4297 , если лямбда-выражение объявляет noexcept спецификацию исключения, а тело лямбда-выражения создает исключение, как показано ниже:

    Дополнительные сведения см. в разделе спецификации исключений (throw).

    Тип возвращаемых данных

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

    Можно опустить часть возвращаемого типа лямбда-выражения, если тело лямбда-выражения содержит только один оператор return или лямбда-выражение не возвращает значение. Если тело лямбда-выражения содержит один оператор return, компилятор выводит тип возвращаемого значения из типа возвращаемого выражения. В противном случае компилятор выводит возвращаемый тип в значение void . Рассмотрим следующие примеры кода, иллюстрирующие этот принцип.

    Лямбда-выражение может создавать другое лямбда-выражение в качестве своего возвращаемого значения. Дополнительные сведения см. в разделе "лямбда-выражения более высокого порядка" в примерах лямбда-выражений.

    Тело лямбда-выражения

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

    Фиксированные переменные из внешней области видимости (см. выше).

    Локально объявленные переменные

    Члены данных класса, объявленные внутри класса и this захваченные

    Любая переменная, которая имеет статическую длительность хранения (например, глобальная переменная)

    В следующем примере содержится лямбда-выражение, которое явно фиксирует переменную n по значению и неявно фиксирует переменную m по ссылке.

    Поскольку переменная n фиксируется по значению, ее значение после вызова лямбда-выражения остается равным 0 . mutable Спецификацию можно n изменить в лямбда-выражении.

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

    Дополнительные сведения см. в разделе Generate.

    В следующем примере кода используется функция из предыдущего примера и добавляется пример лямбда-выражения, использующего алгоритм стандартной библиотеки C++ generate_n . Это лямбда-выражение назначает элемент объекта vector сумме предыдущих двух элементов. mutable Ключевое слово используется, чтобы тело лямбда-выражения может изменить свои копии внешних переменных x и y , которое захватывает лямбда-выражение по значению. Поскольку лямбда-выражение захватывает исходные переменные x и y по значению, их значения остаются равными 1 после выполнения лямбда-выражения.

    Дополнительные сведения см. в разделе generate_n.

    constexpr лямбда-выражения

    Visual Studio 2017 версии 15,3 и более поздних версий (доступно в /std:c++17 ): лямбда-выражение может быть объявлено как constexpr или использоваться в константном выражении, когда инициализация каждого члена данных, который он захватывает или вводит, разрешена в константном выражении.

    Лямбда-выражение неявно, constexpr если его результат удовлетворяет требованиям constexpr функции:

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

    Специально для систем Майкрософт

    Лямбда-выражения не поддерживаются в следующих управляемых сущностях среды CLR: ref class , ref struct , value class или value struct .

    Если используется модификатор, зависящий от Майкрософт, такой как __declspec , его можно вставить в лямбда-выражение сразу после, например parameter-declaration-clause :

    Чтобы определить, поддерживается ли модификатор лямбда-выражениями, см. статью о нем в разделе " модификаторы Microsoft " документации.

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

    Лямбда-функции появились в C++11. Они представляют собой анонимные функции, которые можно определить в любом месте программы, подходящем по смыслу.

    Приведу пример простейшей лямбда функции:

    Выражение auto myLambda означает объявление переменной с автоматически определяемым типом. Крайне удобная конструкция C++11, которая позволяет сделать ваш код более лаконичным и устойчивым к изменениям. Настоящий тип лямбда-функции слишком сложный, поэтому набирать его нецелесообразно.

    Непосредственное объявление лямбда-функции []() < std::cout << "Hello, lambda!" << std::endl; >состоит из трех частей. Первая часть (квадратные скобки []) позволяет привязывать переменные, доступные в текущей области видимости. Вторая часть (круглые скобки ()) указывает список принимаемых параметров лямбда-функции. Третья часть (в фигурных скобках <>) содержит тело лямбда-функции.

    Вызов определенной лямбда-функции ничем не отличается от вызова обычной функции: myLambda() . В нашем случае на консоль будет выведено сообщение:

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

    В качестве аргумента она принимает любой объект, который можно вызвать с аргументом -5. Более подробно о создании таких функций мы говорили, когда рассматривали указатели на функции в C++. Мы будем передавать в call() наши лямбда-функции для запуска.

    Сначала просто выведем переданное лямбда-функции значение:

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

    Обратите внимание на побочный эффект от связывания переменных с лямбда-функцией по ссылке:

    Будьте особенно аккуратны с привязкой параметров по ссылке, когда работаете с циклами. Чтобы не получилось, что все созданные лямбда-функции работали с одним и тем же значением, когда должны иметь собственные копии.

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

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

    Допустимо и комбинирование:

    Когда использовать лямбда-функции?

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

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

    Этот код интуитивно понятен, поэтому вы сами с ним легко разберетесь без моих пояснений. Приведу лишь то, что он выведет на консоль:

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

    Лямбда-функции в C++: Использовать или нет?

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

    И начнем с простого примера. Пусть имеется функция-генератор, которая принимает любой вызываемый объект:

    На практике подобная функция может формировать некую структуру или массив данных. Или выполнять другую полезную работу. Сейчас же someGenerator() просто выводит на консоль числа от 0 до 999, предварительно проводя обработку каждого элемента с помощью f.

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

    При этом для случая ООП-полиморфного решения понадобится класс-Адаптер для функции. Но это лишний код.

    Возможна еще одна ситуация. Пусть в качестве аргумента someGenerator() требуется передать функцию-член. Возникает трудность:

    Здесь я предлагаю сослаться на TDD и гибкие методики разработки ПО в целом. Первое приближение программы должно быть самым простым и кратким. Если понадобится что-то большее, сделаем рефакторинг в будущем. Но не раньше.

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

    В замыкание лямбда-функции мы включили указатель this . Таким образом, для лямбда-функции доступны все поля экземпляра класса MyClass . В том числе и processorFunc() , который мы и вызываем.

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

    Чем меньше область видимости, тем лучше

    Продолжим работу над предыдущим примером. Реализацию, на которой мы остановились, можно упростить еще больше:

    Она стала не только проще, но и надежнее. Но почему? Теперь лямбда-функции доступна лишь очень малая часть информации объекта MyClass . Теперь она не может случайно испортить его состояние.

    Хорошей практикой программирования является обеспечение минимальной области видимости для переменных. Несколько примеров:

    Чем меньше мест, откуда доступна переменная, тем код надежнее, поскольку сокращается число возможностей случайно испортить состояние объекта. Сначала нужно поместить переменную в блок <>. Если этого не хватает, то расширить область видимости до локальной на уровне функции. Если и этого мало, то превратить переменную в private-поле класса. Все, что дальше, уже делать опасно.

    Те же рекомендации распространяются и на функции. Только обычно выбор идет между областями видимости private, protected, public или созданием глобальной функции или функтора.

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

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

    Этот код ищет в массиве строк первый элемент, содержащий подстроку " nut ". Таким образом, он выдает следующий результат:

    И пока он работает, его можно улучшить.

    Корень проблемы здесь в том, что std::find_if требует, чтобы мы передали ей указатель на функцию. Из-за этого мы вынуждены определять функцию, которая будет использоваться только один раз, ей нужно дать имя и поместить в глобальную область видимости (потому что функции не могут быть вложенными!). К тому же эта функция такая короткая, что легче понять, что она делает, по строкам кода, чем по названию и комментариям.

    Лямбды спешат на помощь

    Лямбда-выражение (также называемое лямбда (lambda) или замыкание (closure)) позволяет нам определять анонимную функцию внутри другой функции. Вложенность важна, поскольку она позволяет нам избежать загрязнения пространств имен и определять функцию как можно ближе к тому месту, где она используется (обеспечивая дополнительный контекст).

    Синтаксис лямбда-выражений – одна из самых странных вещей в C++, и к нему нужно немного привыкнуть. Лямбды имеют вид:

    Захват и параметры могут быть пустыми, если они не нужны.

    Тип возвращаемого значения является необязательным, и если он опущен, будет использоваться значение auto (т.е. использование вывода типа для определения типа возвращаемого значения). Хотя мы ранее отмечали, что следует избегать вывода типа для типов, возвращаемых функцией, в этом контексте его можно использовать (поскольку эти функции обычно очень тривиальны).

    Также обратите внимание, что лямбды не имеют имени, поэтому нам не нужно его указывать.

    В качестве отступления.

    Это означает, что определение простейшего лямбда-выражения выглядит так:

    Давайте перепишем приведенный выше пример с помощью лямбда-выражения:

    Это работает так же, как и случай с указателем на функцию, и дает идентичный результат:

    Обратите внимание, насколько похоже наше лямбда-выражение на нашу функцию containsNut . У них обоих одинаковые параметры и тела функций. Лямбда не имеет захвата (что такое захват, мы объясним в следующем уроке), потому что он не нужен. И мы в лямбде опустили завершающий тип возвращаемого значения (для краткости), но поскольку operator!= возвращает bool , наша лямбда также вернет bool .

    Тип лямбды

    В приведенном выше примере мы определили лямбду именно там, где это было необходимо. Такое использование лямбды иногда называют функциональным литералом.

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

    Например, в следующем фрагменте мы используем std::all_of , чтобы проверить, все ли элементы массива четны:

    Мы можем улучшить читаемость следующим образом:

    Обратите внимание, как хорошо читается последняя строка: «вернуть, все ли элементы в массиве четные»

    Но какой тип у лямбды isEven ?

    Оказывается, лямбды не имеют типа, который мы могли бы использовать явно. Когда мы пишем лямбду, компилятор генерирует только для этой лямбды уникальный тип, который нам не предоставляется.

    Для продвинутых читателей

    На самом деле лямбды не являются функциями (что является частью того, как они избегают ограничения C++, не поддерживающего вложенные функции). Это особый вид объектов, называемых функторами. Функторы – это объекты, которые содержат перегруженный operator() , который делает их вызываемыми как функции.

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

    Единственный способ использовать реальный тип лямбды – использовать auto . У auto также есть преимущество в отсутствии дополнительных затрат по сравнению с std::function .

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

    Вывод этой программы:

    Если бы мы использовали auto для типа fn , вызывающий функцию не знал бы, какие параметры и возвращаемый тип должна иметь fn . Кроме того, функции с параметрами auto нельзя разделить на заголовочный и исходный файл. Мы объясняем причину этого ограничения, когда будем говорить о шаблонах.

    Правило

    Используйте auto при инициализации переменных лямбда-выражениями, или std::function , если вы не можете инициализировать переменную лямбда-выражением.

    Обобщенные лямбда-выражения

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

    Одним примечательным исключением является то, что, начиная с C++14, нам разрешено использовать auto для параметров (примечание: в C++20 обычные функции также смогут использовать auto для параметров). Когда лямбда имеет один или несколько параметров auto , компилятор из вызовов лямбды определит, какие типы параметров необходимы.

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

    Для продвинутых читателей

    При использовании в контексте лямбда-выражения auto – это просто сокращение для шаблонного параметра.

    Давайте посмотрим на обобщенную лямбду:

    В приведенном выше примере мы используем параметры auto для захвата наших строк по константной ссылке. Поскольку все строковые типы разрешают доступ к своим отдельным символам через operator[] , нам не нужно заботиться о том, передает ли пользователь std::string , строку в стиле C или что-то еще. Это позволяет нам написать лямбду, которая могла бы принимать любой из этих типов, а это означает, что если мы изменим тип через несколько месяцев, нам не придется переписывать лямбду.

    Однако auto не всегда лучший выбор. Рассмотрим следующий код:

    В этом примере использование auto приведет к выводу типа const char* . Со строками в стиле C нелегко работать (если не считать использования operator[] ). В этом случае мы предпочитаем явно определить параметр как std::string_view , что позволяет нам намного проще работать с базовыми данными (например, мы можем запросить у строкового представления его длину, даже если пользователь передал массив в стиле C).

    Обобщенные лямбды и статические переменные

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

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

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

    Мы можем видеть это в приведенном выше примере, где каждый тип (строковые литералы и целые числа) имеет свой уникальный счетчик! Хотя мы написали лямбду только один раз, было сгенерировано две лямбды, и каждая имеет свою версию callCount . Чтобы иметь общий счетчик для двух сгенерированных лямбда-выражениях, нам нужно определить глобальную переменную или статическую локальную переменную вне лямбда-выражения. Как вы знаете из предыдущих уроков, как глобальные, так и статические локальные переменные могут вызывать проблемы и затруднять понимание кода. Мы сможем избежать этих переменных после того, как поговорим о лямбда-захватах в следующем уроке.

    Вывод возвращаемого типа и завершающие возвращаемые типы

    Если используется вывод типа возвращаемого значения, возвращаемый тип лямбды выводится из инструкций return внутри лямбды. В этом случае все инструкции return в лямбда-выражении должны возвращать один и тот же тип (иначе компилятор не будет знать, какой из них предпочесть).

    Это приводит к ошибке компиляции, поскольку тип возврата первой инструкции return ( int ) не соответствует типу возврата второй инструкции return ( double ).

    В случае, если мы возвращаем разные типы, у нас есть два варианта:

    1. выполните явное приведение, чтобы все возвращаемые типы совпадали, или
    2. явно укажите тип возвращаемого значения для лямбда-выражения и позвольте компилятору выполнить неявные преобразования.

    Второй вариант – обычно лучший выбор:

    Таким образом, если вы когда-нибудь решите изменить тип возвращаемого значения, вам (обычно) нужно будет только изменить тип возвращаемого значения лямбды, и не касаться тела лямбда.

    Функциональные объекты стандартной библиотеки

    Для распространенных операций (например, сложения, отрицания или сравнения) вам не нужно писать свои собственные лямбды, потому что стандартная библиотека поставляется с множеством базовых вызываемых объектов, которые можно использовать вместо этого. Они определены в заголовке <functional> .

    Вместо того чтобы преобразовывать нашу функцию greater в лямбду (что немного скрывает ее значение), мы можем вместо этого использовать std::greater :

    Заключение

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

    Лямбды – это здорово, но они не заменяют обычные функции во всех случаях. Для нетривиальных и многоразовых случаев предпочитайте использование обычных функций.

    Небольшой тест

    Вопрос 1

    Создайте структуру Student , в которой хранятся имя и баллы учащегося. Создайте массив студентов и используйте std::max_element , чтобы найти студента, набравшего наибольшее количество баллов, затем распечатайте имя этого студента. std::max_element принимает начало и конец списка и функцию, которая принимает 2 параметра и возвращает истину, если первый аргумент меньше второго.

    Проверьте код на следующем массиве.

    Ваша программа должна напечатать

    Вопрос 2

    Используйте std::sort и лямбда-выражение в следующем коде, чтобы отсортировать сезоны по возрастанию средней температуры (температура приведена в Кельвинах).

    Читайте также: