О лямбдах ходит много мифов и странных вопросов. Это руководство даст вам более глубокое понимание их правил и того, как они работают.

Перед чтением прочтите, пожалуйста, часть 1 этой серии, иначе некоторые ответы могут не иметь смысла.



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

С одного взгляда:

  • Лямбды дешевы
  • Они размещаются в стеке, а не в куче
  • Их сборка идентична использованию класса
  • Вам не нужно фиксировать статические переменные
  • Если вы хотите вернуть лямбду, используйте std::function или аналогичный
  • Захваченные переменные по умолчанию неизменяемы
  • Копирование лямбды скопирует ее состояние
  • Захват this - не особый случай

Вот что мы расскажем

  • Краткий обзор лямбда-выражений 🎓
  • Ключевые моменты для глубокого понимания лямбд 🗝️
  • Проблемы с производительностью 🚀
  • Каковы правила захвата переменных? 🕸️
  • Как передать лямбду 📧
  • Прочие разные моменты 🌴

Краткое резюме 🎓

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

Примечание по терминологии:

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

Для простоты я буду использовать термин «лямбда» для обозначения этой концепции в целом.

Ключевые моменты для глубокого понимания лямбд 🗝️

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

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

lambda_4a776e7774c8d4ec8eddd924a4a3b251

Таким образом, каждая лямбда создает свой уникальный тип.



Какой ассемблерный код генерирует лямбда?

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

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



Почему я не могу назначить одну лямбду другой?

Потому что типы разные. Подумайте о том, чтобы написать два отдельных класса и назначить их друг другу - это недопустимый C ++.

Проблемы с производительностью 🚀

Насколько дорого стоит лямбда?

Это дешево. (В зависимости от вашего определения дешевизны!)

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

Вычислительно. Если вы не фиксируете какие-либо переменные, это в буквальном смысле вызов функции. Вы не можете получить ничего дешевле!

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

Важно: Стоимость лямбда-выражения никогда не превышает стоимости эквивалентной функции / класса.

Распределяет ли лямбда-выражение в куче?

Нет. Экземпляр функтора будет создан в стеке, как если бы вы создали класс напрямую. Однако размещение std::function слева вместо auto может привести к выделению в куче. Видеть:



Подробнее о std::function позже.

Захват переменных 🕸️

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



Улавливают ли [=] и [&] все окружающие переменные?

Нет, это распространенное заблуждение!

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



Могу ли я иметь значения переменных захвата по умолчанию?

Да, но только в C ++ 14.

Что такого особенного в съемке?

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

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

Почему я не могу зафиксировать статическую ссылку?

Можно в C ++ 14 и выше. Однако лямбда может захватывать только автоматические переменные, а статическая переменная по определению не является автоматической.

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

Обратите внимание, что некоторые компиляторы могут иметь поддержку статического захвата до C ++ 14 и предупреждать как таковые, но все же остается в силе, что вам не нужно их захватывать.



Почему я не могу записывать переменные-члены класса?

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

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

Я хочу фиксировать одни переменные по значению, а другие по ссылке.

Это совершенно верно, используйте следующее:

Передача лямбд и удержание за них 📧

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

Если вы используете C ++ 14 и выше, вы можете установить тип возвращаемого значения auto. Это позволит вам напрямую использовать тип функтора.

В C ++ 11 и ниже вы не можете возвращать auto из функции, что создает проблему для лямбда-типа. Самый простой способ избежать этого - преобразовать его с помощью std::function или какой-либо альтернативной библиотеки, которая может удобно обернуть функцию для вас.

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

Пожалуйста, прочтите следующее для получения дополнительной информации о том, как более поздние версии C ++ позволяют возвращаться автоматически:



И сравнение лямбда-типа auto с типом std::function:



Можно также вернуть лямбда-выражение по указателю на функцию, однако синтаксис ужасен, и вы почти наверняка столкнетесь с проблемами распределения и времени жизни. Я настоятельно рекомендую этого не делать, если только у вас нет очень специфического сценария использования, который auto или std::function не может выполнить.

Что будет, если я скопирую лямбду?

Вы скопируете состояние как есть, как прямое копирование объекта класса (при условии, что он не переопределил конструктор копирования).

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

Прочие разные моменты 🌴

Могу ли я вкладывать лямбды?

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

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

Давайте посмотрим на пример сгенерированного кода из части 1.

Обратите внимание, что в строке 3 объявление operator() имеет значение const, что означает, что оно не может изменять переменные в классе.

Если вам нужно, чтобы это не было константой, вы можете использовать ключевое слово mutable, например:

Обратите внимание, как в lambda1 переменная toMutate была захвачена по значению, поэтому не изменялась вне функции.

Какой тип возвращает лямбда-функция при вызове?

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

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



Могу ли я определить явный тип возвращаемого значения?

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



Заключить

Я рассмотрел здесь то, что я считаю обычными путаницами, связанными с лямбдами. Если вы что-то узнали из этого или у вас есть дополнительные вопросы, оставьте комментарий или напишите мне в Twitter @winwardo, и я обновлю это руководство :)