О лямбдах ходит много мифов и странных вопросов. Это руководство даст вам более глубокое понимание их правил и того, как они работают.
Перед чтением прочтите, пожалуйста, часть 1 этой серии, иначе некоторые ответы могут не иметь смысла.
В части 1 объяснялось, что на самом деле скрывается лямбда, но в ней будет использоваться формат часто задаваемых вопросов и более подробные вопросы. В конце многих вопросов я оставлю ссылку на дальнейшее чтение по обсуждаемой теме.
С одного взгляда:
- Лямбды дешевы
- Они размещаются в стеке, а не в куче
- Их сборка идентична использованию класса
- Вам не нужно фиксировать статические переменные
- Если вы хотите вернуть лямбду, используйте
std::function
или аналогичный - Захваченные переменные по умолчанию неизменяемы
- Копирование лямбды скопирует ее состояние
- Захват
this
- не особый случай
Вот что мы расскажем
- Краткий обзор лямбда-выражений 🎓
- Ключевые моменты для глубокого понимания лямбд 🗝️
- Проблемы с производительностью 🚀
- Каковы правила захвата переменных? 🕸️
- Как передать лямбду 📧
- Прочие разные моменты 🌴
Краткое резюме 🎓
Как мы исследовали в части 1, лямбда - это выражение, которое при вычислении создает локальный класс функтора с operator()
, готовым к вызову. Он также может захватывать переменные, передавая их через конструктор функтора.
Примечание по терминологии:
Технически лямбда - это выражение, определяющее класс функтора. Лямбда относится к классу, как функция - к экземпляру класса.
Для простоты я буду использовать термин «лямбда» для обозначения этой концепции в целом.
Ключевые моменты для глубокого понимания лямбд 🗝️
Что такое лямбда?
Тип лямбды определяется во время компиляции. По сути, вы создаете анонимный класс, и если вы проверите свой вывод на ассемблере, вы увидите, что у него есть имя как таковое:
lambda_4a776e7774c8d4ec8eddd924a4a3b251
Таким образом, каждая лямбда создает свой уникальный тип.
Какой ассемблерный код генерирует лямбда?
Независимо от того, фиксируете вы переменные или нет, это тот же код ассемблера, что и для обычного класса. Единственное исключение состоит в том, что при захвате переменных конструктор встроен, так как он больше нигде не будет использоваться.
Пожалуйста, прочтите следующую статью для более глубокого погружения и доказательства этого:
Почему я не могу назначить одну лямбду другой?
Потому что типы разные. Подумайте о том, чтобы написать два отдельных класса и назначить их друг другу - это недопустимый C ++.
Проблемы с производительностью 🚀
Насколько дорого стоит лямбда?
Это дешево. (В зависимости от вашего определения дешевизны!)
С точки зрения памяти: поскольку лямбда создает класс, это будет так же дорого, как создание эквивалентного класса, который содержит такое же количество переменных, что и вы. Проще говоря, чем больше переменных вы захватите (особенно по значению), тем больше будет ваш сгенерированный класс функтора и тем дороже будет ваше использование лямбда. Если вы обычно захватываете по ссылке, это будет не более чем несколько указателей.
Вычислительно. Если вы не фиксируете какие-либо переменные, это в буквальном смысле вызов функции. Вы не можете получить ничего дешевле!
Если вы захватываете переменную, стоимость такая же, как при построении объекта и вызове функции непосредственно на нем, без виртуального поиска. (Тоже дешево.)
Важно: Стоимость лямбда-выражения никогда не превышает стоимости эквивалентной функции / класса.
Распределяет ли лямбда-выражение в куче?
Нет. Экземпляр функтора будет создан в стеке, как если бы вы создали класс напрямую. Однако размещение std::function
слева вместо auto
может привести к выделению в куче. Видеть:
Подробнее о std::function
позже.
Захват переменных 🕸️
Главное, что нужно знать, это то, что могут быть захвачены только автоматические переменные. Это любая локальная переменная вашей функции, включая указатель this
. Они помещаются в стек и выходят из него и автоматически уничтожаются, когда вы покидаете текущую функцию.
Улавливают ли [=] и [&] все окружающие переменные?
Нет, это распространенное заблуждение!
Они будут захватывать все переменные, которые используются в определении функции, и не более. Однако ваш стиль кодирования может требовать, чтобы вы явно называли каждую захваченную переменную.
Могу ли я иметь значения переменных захвата по умолчанию?
Да, но только в C ++ 14.
Что такого особенного в съемке?
Вообще ничего. this
- это указатель на текущий объект, который фиксируется явно или неявно, как любая другая переменная. Помните, что при написании кода класса вы можете обращаться к переменным-членам, не записывая this->
. Это упущение является причиной некоторой путаницы с лямбдами в классах.
В обоих следующих случаях вы захватываете указатель по значению, а затем разыменовываете его.
Почему я не могу зафиксировать статическую ссылку?
Можно в C ++ 14 и выше. Однако лямбда может захватывать только автоматические переменные, а статическая переменная по определению не является автоматической.
Вам не нужно захватывать статические переменные, поскольку у них всегда один и тот же адрес в памяти. Это не обязательно - просто используйте статическую переменную напрямую.
Обратите внимание, что некоторые компиляторы могут иметь поддержку статического захвата до C ++ 14 и предупреждать как таковые, но все же остается в силе, что вам не нужно их захватывать.
Как лямбда захватывает локальную статическую переменную?
Вам не нужно захватывать глобальные или статические переменные. Если вы хотите использовать… stackoverflow.com , необходимо записывать только автоматические переменные.
Почему я не могу записывать переменные-члены класса?
Вы можете, но помните, что вы можете фиксировать только автоматические переменные. Это означает, что вы не можете явно захватывать переменные-члены, потому что они фактически находятся за разыменованным указателем.
Чтобы получить доступ к переменным-членам, вам необходимо захватить 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, и я обновлю это руководство :)