С. Мейерс - Эффективный и современный C++ (1114942), страница 54
Текст из файла (страница 54)
Это несколько удивительно, так какони не внесли в язык никаких новых возможностей выражения идей. Все, на что способны лямбда-выражения, вы в состоянии сделать вручную, разве что ценой немного больших усилий по вводу с клавиатуры. Но лямбда-выражения представляют собой оченьудобное средство создания функциональных объектов, оказывающее огромное влияниена повседневную разработку программного обеспечения на С++. Без лямбда-выражений алгоритмы "_i f" из STL (например, s t d : : find_i f, s t d : : remove_ i f, s t d : : count_i fи др.) обычно работают с самыми тривиальными предикатами, н о при доступностилямбда-выражений применение этих алгоритмов резко возрастает.
То же самое вернои для алгоритмов, настраиваемых с помощью пользовательской функции сравнения (например, s t d : : sort , std : : nth_element, std : : lower_bound и др.). Вне STL лямбда-выражения позволяют быстро создавать пользовательские удалители для std : : unique_pt rи s t d : : sha red_pt r (см.
разделы 4. l и 4.2) и делают столь же простыми спецификациипредикатов для переменных условий в потоковом API (см. раздел 7.5). Помимо использования в объектах стандартной библиотеки, лямбда-выражения облегчают определениефункций обратного вызова "на лету", функций адаптации интерфейса и контекстно-зависимых функций для разовых вызовов. Лямбда-выражения действительно делают С++более приятным языком программирования.Терминология, связанная с лямбда-выражениями, может обескуражить.
Лямбда-выражение является именно тем, что написано: выражением. Это часть исходного текста.В приведенном ниже фрагменте выделенная полужирным шрифтом часть представляетсобой лямбда-выражение.std : : find_i f ( con tainer. begin ( ) , container. end ( ) ,[ ] (int val) { retшn О < val && val < 10 ; } ) ;•Замь1кание (closure) представляет собой объект времени выполнения, создаваемыйлямбда-выражением. В зависимости от режима захвата замыкания хранят копииссылок на захваченные данные. В приведенном выше вызове std : : find_i f замыкание представляет собой объект, передаваемый во время выполнения в std : : find_ i fв качестве третьего аргумента.•Класс замыкания (closure class) представляет собой класс, из котороrо инстанцируется замыкание.
Каждое лямбда-выражение заставляет компиляторы генерироватьуникальный класс замыкания. Инструкции внутри лямбда-выражения становятсявыполнимыми инструкциями функций-членов их класса замыкания.Лямбда-выражения часто используются для создания замыкания, которое применяется только в качестве аргумента функции. Именно этот случай представлен в приведенном выше примере std : : find_ if. Однако в общем случае замыкания могут копироваться, так что обычно можно иметь несколько замыканий типа замыкания, соответствующего одному лямбда-выражению. Например, в коде11 х - локальная переменнаяint х ;auto cl[ х ] ( int у ) {return хcl;auto с2с2;auto сЗ=*1111у > 5 5 ; ) ; 111111c l - копия замыкания ,сгенерированноголямбда-выражениемс2копия clсЗ - копия с2и сЗ являются копиями замыкания, порожденного лямбда-выражением.Неформально вполне приемлемо размытие границы между лямбда-выражениями, замыканиями и классами замыканий.
Но в последующих разделах очень часто важно различать, что существует во время компиляции (лямбда-выражения и классы замыканий),что - во время выполнения (замыкания) и как они соотносятся одно с другим.cl, с26.1 . Избеrайте режимов захвата по умопчаниюВ С++ 1 1 имеются два режима захвата: по ссылке и по значению. Захват по умолчаниюпо ссылке может привести к висячим ссылкам. Захват по умолчанию по значению соблазняет считать, что вы не подвержены этой проблеме (это не так), и думать, что вашизамыкания являются самодостаточными автономными замыканиями (но они могут и небыть таковыми).Так выглядят основные положения данного раздела. Если вы в большей степени творец, чем ремесленник, вы, вероятно, захотите узнать побольше - так давайте начнемс опасности захвата по ссылке.Захват по ссылке приводит к тому, что замыкание содержит ссылку на локальнуюпеременную или на параметр, доступный в области видимости, где определено лямбдавыражение.
Если время жизни замыкания, созданного из лямбда-выражения, превышаетвремя жизни локальной переменной или параметра, ссылка в замыкании оказываетсявисячей. Предположим, например, что у нас есть контейнер с функциями фильтрации,каждая из которых принимает int и возвращает bool, указывающий, удовлетворяет липереданное значение фильтру:222Гnава 6. Лямбда-выражения11 См.
using в разделе 3 . 3 , std: : function - в 2 . 1 :using FilterContainer = std : : vector<std : : function<bool ( int ) >>;FilterContainer filters; / / Функции фильтрацииМы можем добавить фильтр для чисел, кратных 5, следующим образом:filters . ernplace_back (/ / См . ernplace_back в разделе 8 . 2[ ] ( int value ) { return value % 5 == О ; });Однако может быть так, что нам нужно вычислять делитель во время выполнения,т.е. мы не можем жестко "прошить" значение 5 в лямбда-выражении.
Поэтому добавление фильтра может выглядеть следующим образом:void addDivi sorFilter ( )auto calcl = cornputeSorneValuel ( ) ;auto calc2cornputeSorneValue2 ( ) ;auto divisorcornputeDivisor ( calc l , calc2 ) ;filters . ernplace_back ( / / Опасно ! Ссылка на divi sor повиснет ![&] ( int value ) { return value % divisor == О ; }) ;==Этот код - бомба замедленного действия.
Лямбда-выражение ссылается на локальную переменную divi sor, но эта переменная прекращает свое существование после выхода из addDivi sorFi l te r. Этот выход осуществляется сразу после выхода изf i l ters . ernplace_back, так что добавленная в f i lters функция, по сути, является мертворожденной. Применение этого фильтра ведет к неопределенному моменту практически с момента создания.Та же проблема имеет место и при явном захвате di visor по ссылке,filters . ernplace_back ([ &divisor] ( int va lue ){ return value % divisor) ;11 Опасно ! Ссылка наО ; ) // divisor все равно11 повисает !но при явном захвате проще увидеть, что жизнеспособность лямбда-выражения зависитот времени жизни di vi sor.
Кроме того, использование имени di visor напоминает намо необходимости убедиться, что di vi sor существует как минимум столько же времени,сколько и замыкание лямбда-выражения. Это более конкретное напоминание, чем обобщенное "убедитесь, что ничего не висит", о чем гласит конструкция [ & ] .Если вы знаете, что замыкание будет использовано немедленно (например, будучипереданным в алгоритм STL) и не будет копироваться, нет никакого риска, что ссылки,которые оно хранит, переживут локальные переменные и параметры в среде, где создано это лямбда-выражение. Вы могли бы утверждать, что в этом случае нет риска получить висячие ссылки, а следовательно, нет причин избегать режима захвата по ссылке6.1 .Избегайте режимов захвата по умопчанию223по умолчанию.
Например, наше фильтрующее лямбда-выражение может использоватьсятолько как аргумент в алгоритме С++ 1 1 s t d : : a l l _ of, который проверяет, все ли элементы диапазона удовлетворяют условию:template<typename С>void workWithContainer ( const С& container)auto calclcomputeSomeValue l ( ) ;11 Как и ранее11 Как и ранееauto calc2computeSomeVal ue2 ( ) ;auto clivisorcomputeDivisor ( ca l c l , calc2 ) ; / / Как и ранее===11 Тип элементов в контейнере :using ContElemT=typename C : : value type ;using std : : begin;using std : : end;11 Для обобщенности ;11 см.
раздел 3 . 7i f ( std: : all_of (11begin ( container ) ,11end ( container) ,11[ &] ( const ContElemT& value ){ return value % divisor) {11else {Все значенияв контейнерекратны divisor?О; ) )Да11 Как минимум одно - нетДа, все так, это безопасно, но эта безопасность довольно неустойчива.
Если выяснится, что это лямбда-выражение полезно в друтих контекстах (например, как функция, добавленная в контейнер f i l t ers) и будет скопировано и вставлено в контекст, где этозамыкание может пережить di v i s o r, вы вновь вернетесь к повисшим ссылкам, и в выражении захвата не будет ничего, что напомнило бы вам о необходимости анализа временижизни di vi s or .С точки зрения долгосрочной перспективы лучше явно перечислять локальные переменные, от которых зависит лямбда-выражение.Кстати, возможность применения auto в спецификации параметров лямбда-выражений в С++ 1 4 означает, что приведенный выше код можно записать проще при использовании С++ 1 4.
Определение псевдонима типа Con t E l emT можно убрать, а условие i fможет быть переписано следующим образом:if ( std : : all_of (begin ( container ) , end ( container ) ,[ & ] ( const auto& value )О; ) ) ){ return value % divisor==224Гла ва 6. Лямбда-выражения!/ C++ l 4Одним из способов решения нашей проблемы с локальной переменной divi s o r может быть применение режима захвата по умолчанию по значению, т.е. мы можем добавить лямбда-выражение к f i l t e r s следующим образом:filters . emplace_back ([=] ( iпt value ){ return value % divisor);О; }11111111Теперьdivisorн е можетзависнутьДля данного примера этого достаточно, но в общем случае захват по умолчаниюпо значению не является лекарством от висящих ссылок, как вам могло бы показаться.Проблема в том, что если вы захватите указатель по значению, то скопируете его в замыкания, возникающие из лямбда-выражения, но не сможете предотвратить освобождениеобъекта, на который он указывает (и соответственно, повисания), внешним кодом."Этого не может случиться! - возразите вы.