Главная страница

Язык программирования C Пятое издание


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница27 из 54
1   ...   23   24   25   26   27   28   29   30   ...   54
Упражнение 10.10. Почему алгоритмы не изменяют размер контейнеров?
10.3. Перенастройка функций
Большинство алгоритмов сравнивает элементы исходной последовательности. По умолчанию такие алгоритмы используют оператор < или == типа элемента. Библиотека предоставляет также версии этих алгоритмов, позволяющие использовать собственный оператор вместо заданного по умолчанию.
Например, алгоритм sort() использует оператор < типа элемента. Но может понадобиться сортировать последовательность в порядке, отличном от определенного оператором <,
либо у типа элемента последовательности может не быть оператора < (как у класса
Sales_data). В обоих случаях необходимо переопределить стандартное поведение функции sort().
10.3.1. Передача функций алгоритму
Предположим, например, что необходимо вывести вектор после вызова функции elimDups()
(см. раздел 10.2.3). Однако слова должны быть упорядочены сначала по размеру, а затем в алфавитном порядке в пределах каждого размера. Чтобы переупорядочить вектор по длине слов, используем вторую перегруженную версию функции sort(). Она получает третий аргумент, называемый предикатом. Предикаты
Предикат (predicate) — это допускающее вызов выражение, возвращающее значение,
применимое в условии. Библиотечные алгоритмы используют унарные предикаты (unary predicate) (с одним параметром) или бинарные предикаты (binary predicate) (с двумя параметрами). Получающие предикаты алгоритмы вызывают его для каждого элемента в исходном диапазоне. Поэтому тип элемента должен допускать преобразование в тип параметра предиката.
Версия функции sort(), получающей бинарный предикат, использует его вместо оператора <
при сравнении элементов. Предикаты, предоставляемые функции sort(), должны соответствовать требованиям, описанным в разделе 11.2.2, а пока достаточно знать, что он
Page 486/1103
должен определить единообразный порядок для всех возможных элементов в исходной последовательности. Функция isShorter() из раздела 6.2.2 — хороший пример функции,
соответствующей этим требованиям. Таким образом, функцию isShorter() можно передать как предикат алгоритму sort(). Это переупорядочит элементы по размеру:
// функция сравнения, используемая при сортировке слов по длине bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size();
}
// сортировка слов по длине от коротких к длинным sort(words.begin(), words.end(), isShorter);
Если контейнер words содержит те же данные, что и в разделе 10.2.3, то этот вызов переупорядочит его так, что все слова длиной 3 символа расположатся перед словами длиной 4 символа, которые в свою очередь расположатся перед словами длиной 5 символов,
и т.д. Алгоритмы сортировки
При сортировке вектора words по размеру следует также обеспечить алфавитный порядок элементов одинаковой длины. Для обеспечения алфавитного порядка можно использовать алгоритм stable_sort(), обеспечивающий первоначальный порядок сортировки среди равных элементов.
Обычно об относительном порядке равных элементов можно не заботиться. В конце концов,
они ведь равны. Но в данном случае под "равными" подразумеваются элементы со значениями одинаковой длины. Элементы одинаковой длины все еще отличаются друг от друга при просмотре их содержимого. Вызов функции stable_sort() позволяет расположить элементы с одинаковыми значениями длины в алфавитном порядке: elimDups(words); // расположить слова в алфавитном порядке
// и удалить дубликаты
// пересортировать по длине, поддерживая алфавитный порядок среди слов
// той же длины stable_sort(words.begin(), words.end(), isShorter); for (const auto &s : words) // копировать строки не нужно
Page 487/1103
cout << s << " "; // вывести каждый элемент, отделяя его пробелом cout << endl;
Предположим, что перед этим вызовом вектор words был отсортирован в алфавитном порядке. После вызова он будет отсортирован по размеру элемента, а слова одинаковой длины остаются в алфавитном порядке. Если выполнить этот код для первоначального содержимого вектора, то результат будет таким: fox red the over slow jumps quick turtle Упражнения раздела 10.3.1
Упражнение 10.11. Напишите программу, использующую функции stable_sort() и isShorter()
для сортировки вектора, переданного вашей версии функции elimDups(). Для проверки правильности программы выведите содержимое вектора.
Упражнение 10.12. Напишите функцию compareIsbn(), которая сравнивает члены isbn двух объектов класса Sales_data. Используйте эту функцию для сортировки вектора объектов класса Sales_data.
Упражнение 10.13. Библиотека определяет алгоритм partition(), получающий предикат и делящий контейнер так, чтобы значения, для которых предикат возвращает значение true,
располагались в начале последовательности, а для которых он возвращает значение false —
в конце. Алгоритм возвращает итератор на следующий элемент после последнего, для которого предикат возвратил значение true. Напишите функцию, которая получает строку и возвращает логическое значение, указывающее, содержит ли строка пять символов или больше. Используйте эту функцию для разделения вектора words. Выведите элементы, у которых есть пять или более символов.
10.3.2. Лямбда-выражения
У передаваемых алгоритмам предикатов должен быть точно один или два параметра, в зависимости от того, использует ли алгоритм унарный или бинарный предикат соответственно. Но иногда необходима обработка, которая требует большего количества аргументов, чем позволяет предикат алгоритма. Например, решение для последнего упражнения в предыдущем разделе имело жестко заданный размер в 5 символов, согласно которому предикат делил последовательность. Было бы удобней иметь возможность разделять последовательность без необходимости писать отдельный предикат для каждого возможного размера.
Для примера пересмотрим программу из раздела 10.3.1 так, чтобы вывести количество слов с указанным размером или больше него. Вывод изменим так, чтобы он сообщал только те слова, длина которых равна или больше заданной.
Вот "эскиз" этой функции, которую мы назовем biggies(): void biggies(vector<string> &words, vector<string>::size_type sz) { elimDups(words); //
Page 488/1103
расположить слова в алфавитном порядке
// и удалить дубликаты
// пересортировать по длине, поддерживая алфавитный порядок среди слов
// той же длины stable_sort(words.begin(), words.end(), isShorter);
// получить итератор на первый элемент, размер которого >= sz
// вычислить количество элементов с размером >= sz
// вывести слова, размер которых равен или больше заданного, разделяя
// их пробелами
}
Новая проблема — поиск первого элемента вектора заданного размера. Как известно, чтобы выяснить количество элементов, размер которых равен или больше заданного, можно использовать их позицию.
Для поиска элементов определенного размера можно использовать библиотечный алгоритм find_if(). Подобно функции find() (см. раздел 10.1), алгоритм find_if() получает два итератора,
обозначающих диапазон. В отличие от функции find(), третий аргумент функции find_if()
является предикатом. Алгоритм find_if() вызывает переданный предикат для каждого элемента в исходном диапазоне. Он возвращает первый элемент, для которого предикат возвращает отличное от нуля значение, или конечный итератор, если ни один подходящий элемент не найден.
Совсем не сложно написать функцию, которая получает строку и размер, а возвращает логическое значение, означающее, не превосходит ли размер данной строки указанный.
Однако функция find_if() получает унарный предикат, поэтому любая передаваемая ей функция, которая может быть вызвана с элементом исходной последовательности, должна иметь только один параметр. Нет никакого способа передать ей второй аргумент,
представляющий размер. Чтобы решить эту часть проблемы, используем некоторые дополнительные средства языка. Знакомство с лямбда-выражением
Алгоритму можно передать любой вид вызываемого объекта (callable object). Объект или выражение является вызываемым, если к нему можно применить оператор вызова (см. раздел 1.5.2). Таким образом, если е —
Page 489/1103
вызываемое выражение, то можно написать е( аргументы ), где аргументы — это разделяемый запятыми список любого количества аргументов.
Единственными вызываемыми объектами, использованными до сих пор, были функции и указатели на функции (см. раздел 6.7). Есть еще два вида вызываемых объектов: классы,
перегружающие оператор вызова функции (будут рассматриваться в разделе 14.8), и лямбда-выражения (lambda expression).
Лямбда-выражение представляет собой вызываемый блок кода. Его можно считать безымянной встраиваемой функцией. Подобно любой функции, у лямбда-выражений есть тип возвращаемого значения, список параметров и тело функции. В отличие от функции,
лямбда-выражения могут быть определены в функции. Форма лямбда-выражений такова:
[ список захвата ]( список параметров ) -> тип возвращаемого значения
{ тело функции } где список захвата (зачастую пустой) — это список локальных переменных, определенных в содержащей функции; тип возвращаемого значения , список параметров и тело функции — те же самые, что и у любой обычной функции. Однако, в отличие от обычных функций, для определения типа возвращаемого значения лямбда-выражения должны использовать замыкающий тип (см. раздел 6.3.3).
Список параметров и типа возвращаемого значения могут отсутствовать, но список захвата и тело функции должны быть всегда: auto f = [] { return 42; };
Здесь f определено как вызываемый объект, не получающий никаких аргументов и возвращающий значение 42. Вызов лямбда-выражений происходит таким же способом, что и вызов функций, — при помощи оператора вызова: cout << f() << endl; // выводит 42
Пропуск круглых скобок и списка параметров в лямбда-выражении эквивалентен определению пустого списка параметров. Следовательно, когда происходит вызов лямбда-выражения f, список аргументов оказывается пустым. Если пропущен тип
Page 490/1103
возвращаемого значения, то выведенный тип возвращаемого значения лямбда-выражения будет зависеть от кода в теле функции. Если телом является только оператор return, тип возвращаемого значения выводится из типа возвращаемого выражения. В противном случае типом возвращаемого значения является void.
Лямбда-выражения, тела которых содержат нечто кроме одного оператора return, что не определяет тип возвращаемого значения, возвращают тип void. Передача аргументов лямбда-выражению
Подобно вызовам обычных функций, аргументы вызова лямбда-выражения используются для инициализации его параметров. Как обычно, типы аргумента и параметра должны совпадать.
В отличие от обычных функций, у лямбда-выражений не может быть аргументов по умолчанию (см. раздел 6.5.1). Поэтому у вызова лямбда-выражения всегда столько аргументов, сколько и параметров. Как только параметры инициализируются, выполняется тело лямбда-выражения.
Для примера передачи аргументов можно написать лямбда-выражение, ведущее себя как функция isShorter():
[](const string &a, const string &b)
{ return a.size() < b.size();}
Пустой список захвата означает, что это лямбда-выражение не будет использовать локальных переменных из окружающей функции. Параметры лямбда-выражения, как и параметры функции isShorter(), будут ссылкой на константную строку. Как и тело функции isShorter(), тело лямбда-выражения сравнивает размер параметров и возвращает логическое значение, зависящее от соотношения размеров своих аргументов.
Вызов функции stable_sort() можно переписать так, чтобы использовать это лямбда-выражение следующим образом:
// сортировать слова по размеру, поддерживая алфавитный порядок среди
// слов того же размера stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size();});
Когда функция stable_sort() будет сравнивать два элемента, она вызовет данное лямбда-выражение. Использование списка захвата
Теперь все готово для решения первоначальной задачи — создания вызываемого выражения, которое можно передать функции find_if(). Необходимо выражение,
сравнивающее длину каждой строки в исходной последовательности со значением параметра sz функции biggies().
Хотя лямбда-выражение может присутствовать в функции, оно способно использовать локальные переменные этой функции,
Page 491/1103
только заранее определив, какие из них предстоит использовать. Лямбда-выражение определяет подлежащие использованию локальные переменные, включив их в список захвата (capture list). Список захвата предписывает лямбда-выражению включить информацию, необходимую для доступа к этим переменным, в само лямбда-выражение.
В данном случае лямбда-выражение захватит переменную sz и будет иметь один параметр типа string. Тело лямбда-выражения сравнивает размер переданной строки с захваченным значением переменной sz:
[sz](const string &a)
{ return a.size () >= sz; };
В начинающих лямбда-выражение квадратных скобках, [], можно расположить разделяемый запятыми список имен, определенных в окружающей функции.
Поскольку данное лямбда-выражение захватывает переменную sz, ее можно будет использовать в теле лямбда-выражения. Лямбда-выражение не захватывает вектор words,
поэтому доступа к его переменным она не имеет. Если бы лямбда-выражение имело пустой список захвата, наш код не компилировался бы:
// ошибка: sz не захвачена
[](const string &a)
{ return a.size() >= sz; };
Лямбда-выражение может использовать локальную переменную окружающей функции,
только если она присутствует в ее списке захвата. Вызов функции find_if()
Используя это лямбда-выражение, можно найти первый элемент, размер которого не меньше sz:
// получить итератор на первый элемент, размер которого >= sz auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{ return a.size() >= sz; });
Вызов функции find_if() возвращает итератор на первый элемент, длина которого не меньше sz, или на элемент words.end(), если такового элемента не существует.
Возвращенный функцией find_if() итератор можно использовать для вычисления количества элементов, расположенных между этим итератором и концом вектора words (см. раздел
3.4.2):
// вычислить количество элементов с размером >= sz auto count = words.end() - wc;
Page 492/1103
cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
Для вывода в сообщении слова word или words, в зависимости от того, равен ли размер 1,
оператор вывода вызывает функцию make_plural() (см. раздел 6.3.2). Алгоритм for_each()
Последняя часть задачи — вывод тех элементов вектора words, длина которых не меньше sz.
Для этого используем алгоритм for_each(), получающий вызываемый объект и вызывающий его для каждого элемента в исходном диапазоне:
// вывести слова, размер которых равен или больше заданного, разделяя
// их пробелами for_each(wc, words.end(),
[](const string &s) {cout << s << " ";}); cout << endl;
Список захвата этого лямбда-выражения пуст, но все же его тело использует два имени: его собственный параметр s и cout.
Список захвата пуст, поскольку он используется только для нестатических переменных,
определенных в окружающей функции. Лямбда-выражение вполне может использовать имена, определенные вне той функции, в которой присутствует лямбда-выражение. В данном случае имя cout не локально определено в функции biggies(), оно определено в заголовке iostream. Пока заголовок iostream находится в области видимости функции biggies(), данное лямбда-выражение может использовать имя cout.
Список захвата используется только для локальных нестатических переменных;
лямбда-выражения могут непосредственно использовать статические локальные переменные и переменные, объявленные вне функции. Объединив все вместе
Теперь, изучив элементы программы подробно, рассмотрим ее в целом: void biggies(vector<string> &words, vector<string>::size_type sz) { elimDups(words); // расположить слова в алфавитном порядке
// и удалить дубликаты
// пересортировать по длине, поддерживая алфавитный порядок среди слов
//
Page 493/1103
той же длины stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size(); });
// получить итератор на первый элемент, размер которого >= sz auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{ return a.size() >= sz; });
// вычислить количество элементов с размером >= sz auto count = words.end() - wc; cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
// вывести слова, размер которых равен или больше заданного, разделяя
// их пробелами for_each(wc, words.end(),
[](const string &s) {cout << s << " ";}); cout << endl;
} Упражнения раздела 10.3.2
Упражнение 10.14. Напишите лямбда-выражение, получающее два целых числа и возвращающее их сумму.
Упражнение 10.15. Напишите лямбда-выражение, захватывающее переменную типа int окружающей функции и получающей параметр типа int. Лямбда-выражение должно возвратить сумму захваченного значения типа int и параметра типа int.
Упражнение 10.16. Напишите собственную версию функции biggies(), используя лямбда-выражения.
Упражнение 10.17. Перепишите упражнение 10.12 из раздела 10.3.1 так, чтобы в вызове функции sort() вместо функции compareIsbn() использовалось лямбда-выражение.
Упражнение 10.18. Перепишите функцию biggies() так, чтобы использовать алгоритм partition()
вместо алгоритма find_if(). Алгоритм partition() описан в упражнении 10.13 раздела 10.3.1.
Page 494/1103

Упражнение 10.19. Перепишите предыдущее упражнение так, чтобы использовать алгоритм stable_partition(), который, подобно алгоритму stable_sort(), обеспечивает исходный порядок элементов в разделяемой последовательности.
10.3.3. Захват и возвращение значений лямбда-выражениями
При определении лямбда-выражения компилятор создает новый (безымянный) класс,
соответствующий этому лямбда-выражению. Создание этих классов рассматривается в разделе 14.8.1, а пока следует понять, что при передаче лямбда-выражения функции определяется новый тип и создается его объект. Безымянный объект этого созданного компилятором типа и передается как аргумент. Аналогично при использовании ключевого слова auto для определения переменной, инициализированной лямбда-выражением,
определяется объект типа, созданного из этого лямбда-выражения.
По умолчанию созданный из лямбда-выражения класс содержит переменные-члены,
соответствующие захваченным переменным лямбда-выражения. Подобно переменным-членам любого класса, переменные-члены лямбда-выражения инициализируются при создании его объекта. Захват по значению
Подобно передаче параметров, переменные можно захватывать по значению или по ссылке.
В табл. 10.1 приведены различные способы создания списка захвата. До сих пор у использованных лямбда-выражений захват переменных осуществлялся по значению.
Подобно передаче по значению параметров, захват переменной по значению подразумевает ее копирование. Но, в отличие от параметров, копирование значения при захвате осуществляется при создании лямбда-выражения, а не при его вызове: void fcnl() { size_t v1 = 42; // локальная переменная
// копирует v1 в вызываемый объект f auto f = [v1] { return v1; }; v1 = 0; auto j = f(); // j = 42; f получит копию v1 на момент создания
}
Поскольку значение копируется при создании лямбда-выражения, последующие изменения захваченной переменной никак не влияют на соответствующее значение в лямбда-выражении.
Таблица 10.1. Список захвата лямбда-выражения [] Пустой список захвата.
Лямбда-выражение не может использовать переменные из содержащей функции.
Page 495/1103

Лямбда-выражение может использовать локальные переменные, только если оно захватывает их [ names ] names — разделяемый запятыми список имен, локальных для содержащей функции. По умолчанию переменные в списке захвата копируются. Имя, которому предшествует знак
&, захватывается по ссылке [&] Неявный захват по ссылке. Сущности из содержащей функции используются в теле лямбда-выражения по ссылке [=] Неявный захват по значению. Сущности из содержащей функции используются в теле лямбда-выражения как копии [&, identifier_list ] identifier_list — разделяемый запятыми список любого количества переменных из содержащей функции. Эти переменные захватываются по значению; любые неявно захваченные переменные захватывается по ссылке. Именам в списке identifier_list не могут предшествовать символы & [=, reference_list ] Переменные, включенные в список reference_list , захватываются по ссылке; любые неявно захваченные переменные захватывается по значению. Имена в списке reference_list не могут включать часть this и должны предваряться символом & Захват по ссылке
Можно также определять лямбда-выражения, захватывающие переменные по ссылке.
Например: void fcn2() { size_t v1 = 42; // локальная переменная
// объект f2 содержит ссылку на v1 auto f2 = [&v1] { return v1; }; v1 = 0; auto j = f2(); // j = 0; f2 ссылается на v1; он не хранится в j
}
Символ & перед v1 указывает, что переменная v1 должна быть захвачена как ссылка.
Захваченная по ссылке переменная действует так же, как любая другая ссылка. При использовании переменной в теле лямбда-выражения фактически применяется объект, с которым связана эта ссылка. В данном случае, когда лямбда-выражение возвращает v1,
возвращается значение объекта, на который ссылается переменная v1.
Захват ссылок имеет те же проблемы и ограничения, что и возвращение ссылок (см. раздел
Page 496/1103

6.1.1). При захвате переменной по ссылке следует быть уверенным , что объект, на который она ссылается, существует на момент выполнения лямбда-выражения. Переменные, захваченные лямбда-выражением, являются локальными,
они перестают существовать сразу, как только функция завершится. Если лямбда-выражение продолжит выполняться после завершения функции, то используемые ею локальные переменные окажутся несуществующими.
Иногда захват по ссылке необходим. Например, может понадобиться, чтобы функция biggies()
получала ссылку на поток ostream для записи символа, используемого как разделитель: void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') {
// код, переупорядочивающий слова как прежде
// оператор вывода количества, пересмотренный для вывода os for_each(words.begin(), words.end(),
[&os, c](const string &s) { os << s << c; });
}
Объекты потока ostream нельзя копировать (см. раздел 8.1.1); единственный способ захвата объекта os — это ссылка (или указатель).
При передаче лямбда-выражения функции, как и в случае вызова функции for_each(),
лямбда-выражение выполняется немедленно. Захват объекта os по ссылке хорош потому,
что переменные в функции biggies() существуют во время выполнения функции for_each().
Лямбда-выражение можно также возвратить из функции. Функция может возвратить вызываемый объект непосредственно или возвратить объект класса, у которого вызываемый объект является переменной-членом. Если функция возвращает лямбда-выражение, то по тем же причинам, по которым функция не должна возвращать ссылку на локальную переменную, лямбда-выражение не должно содержать захваченных ссылок.
Когда переменная захвачена по ссылке, следует удостовериться, что эта переменная существует во время выполнения лямбда-выражения. Совет. Не усложняйте списки захвата лямбда-выражений
Механизм захвата лямбда-выражения хранит полученную информацию между моментом создания лямбда-выражение (т.е. когда выполняется код определения лямбда-выражения) и моментом собственно выполнения лямбда-выражения. Разработчику следует самостоятельно позаботиться о том, чтобы независимо от момента захвата информации она осталась достоверной на момент выполнения лямбда-выражения.
Захват обычной переменной (типа int, string и так далее, но не указателя) обычно достаточно прост. В данном случае следует позаботиться о наличии у переменной значения в момент ее захвата.
Page 497/1103

При захвате указателя, итератора или переменной по ссылке следует удостовериться, что связанный с ними объект все еще существует на момент выполнения лямбда-выражения. Кроме того, объект в этот момент гарантированно должен иметь значение. Код, выполняемый между моментом создания лямбда-выражения и моментом его выполнения, может изменить значение объекта, на который указывает (или ссылается) захваченная сущность. Во время захвата указателя (или ссылки) значение объекта, возможно, и было правильным, но ко времени выполнения лямбда-выражения оно могло измениться.
Как правило, сокращая объем захватываемых данных, потенциальных проблем с захватом можно избежать. Кроме того, по возможности избегайте захвата указателей и ссылок.
Неявный захват
Вместо предоставления явного списка переменных содержащей функции, которые предстоит использовать, можно позволить компилятору самостоятельно вывести используемые переменные из кода тела лямбда-выражения. Чтобы заставить компилятор самостоятельно вывести список захвата, в нем используется символ & или =. Символ & указывает,
что предполагается захват по ссылке, а символ = — что значения захватываются по значению. Например, передаваемое функции find_if() лямбда-выражение можно переписать так:
// sz неявно захватывается по значению wc = find_if(words.begin(), words.end(),
[=](const string &s)
{ return s.size () >= sz; });
Если одни переменные необходимо захватить по значению, а другие по ссылке, вполне можно совместить явный и неявный захваты: void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = cout, char c = ' ') {
// другие действия, как прежде
// os неявно захватывается по ссылке;
// с явно захватывается по значению for_each(words.begin(), words.end(),
[&, c](const string &s) { os << s << c; });
//
Page 498/1103
os явно захватывается по ссылке;
// с неявно захватывается по значению for_each(words.begin(), words.end(),
[=, &os](const string &s) { os << s << c; });
}
При совмещении неявного и явного захвата первым элементом в списке захвата должен быть символ & или =. Эти символы задают режим захвата по умолчанию: по ссылке или по значению соответственно.
При совмещении неявного и явного захвата явно захваченные переменные должны использовать дополнительную форму. Таким образом, при неявном захвате по ссылке (с использованием &) явно именованные переменные должны захватываться по значению;
следовательно, их именам не может предшествовать символ &. И наоборот, при неявном захвате по значению (с использованием =) явно именованным переменным должен предшествовать символ &, означающий, что они должны быть захвачены по ссылке.
Изменяемые лямбда-выражения
По умолчанию лямбда-выражение не может изменить значение переменной, которую она копирует по значению. Чтобы изменить значение захваченной переменной, за списком параметров должно следовать ключевое слово mutable. Изменяемые лямбда-выражения не могут пропускать список параметров: void fcn3() { size_t v1 = 42; // локальная переменная
// f может изменить значение захваченных переменных auto f = [v1]() mutable { return ++v1; }; v1 = 0; auto j = f(); // j = 43
}
Может ли захваченная по ссылке переменная быть изменена, зависит только от того,
ссылается ли она на константный или неконстантный тип: void fcn4() { size_t v1 = 42; // локальная переменная
Page 499/1103

// v1 - ссылка на неконстантную переменную
// эту переменную можно изменить в f2 при помощи ссылки auto f2 = [&v1] { return ++v1; }; v1 = 0; auto j = f2(); // j = 1
} Определение типа возвращаемого значения лямбда-выражения
Использованные до сих пор лямбда-выражения содержали только один оператор return. В
результате тип возвращаемого значения определять было не нужно. По умолчанию, если тело лямбда-выражения содержало какие-нибудь операторы, кроме оператора return, то подразумевалось, что оно возвращало тип void. Подобно другим функциям, возвращающим тип void, подобные лямбда-выражения могут не возвращать значения.
В качестве примера используем библиотечный алгоритм transform() и лямбда-выражение для замены каждого отрицательного значения в последовательности его абсолютным значением: transform(vi.begin(), vi.end(), vi.begin(),
[](int i) { return i < 0 ? -i : i; });
Функция transform() получает три итератора и вызываемый объект. Первые два итератора обозначают исходную последовательность, третий итератор — назначение. Алгоритм вызывает переданный ему вызываемый объект для каждого элемента исходной последовательности и записывает результат по назначению. Как и в данном примере,
итератор назначения может быть тем же, обозначающим начало ввода. Когда исходный итератор и итератор назначения совпадают, алгоритм transform() заменяет каждый элемент в исходном диапазоне результатом вызова вызываемого объекта для этого элемента.
В этом вызове передавалось лямбда-выражение, которое возвращает абсолютное значение своего параметра. Тело лямбда-выражения — один оператор return, который возвращает результат условного выражения. Необходимости определять тип возвращаемого значения нет, поскольку его можно вывести из типа условного оператора.
Но если написать на первый взгляд эквивалентную программу, используя оператор if, то код не будет компилироваться:
// ошибка: нельзя вывести тип возвращаемого значения лямбда-выражения transform(vi.begin(), vi.end(), vi.begin(),
[](int i) {if (i < 0) return -i; else return i; });
Эта версия лямбда-выражения выводит тип возвращаемого значения как void, но возвращает значение.
Page 500/1103

Когда необходимо определить тип возвращаемого значения для лямбда-выражения, следует использовать замыкающий тип возвращаемого значения (см. раздел 6.3.3): transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{ if (i < 0) return -i; else return i; });
В данном случае четвертым аргументом функции transform() является лямбда-выражение с пустым списком захвата, единственным параметром типа int и возвращаемым значением типа int. Его телом является оператор if, возвращающий абсолютное значение параметра.
Упражнения раздела 10.3.3
Упражнение 10.20. Библиотека определяет алгоритм count_if(). Подобно алгоритму find_if(), он получает пару итераторов, обозначающих исходный диапазон и предикат, применяемый к каждому элементу заданного диапазона. Функция count_if() возвращает количество раз, когда предикат вернул значение true. Используйте алгоритм count_if(), чтобы переписать ту часть программы, которая рассчитывала количество слов длиной больше 6.
Упражнение 10.21. Напишите лямбда-выражение, которое захватывает локальную переменную типа int и осуществляет декремент ее значения, пока оно не достигает 0. Как только значение переменной достигнет 0, декремент переменной прекращается.
Лямбда-выражение должно возвратить логическое значение, указывающее, имеет ли захваченная переменная значение 0.
10.3.4. Привязка аргументов
Лямбда-выражения особенно полезны для простых операций, которые не предполагается использовать в более чем одном или двух местах. Если ту же операцию необходимо осуществлять во многих местах, то обычно определяют функцию, а не повторяют то же лямбда-выражение многократно. Аналогично, если операция требует многих операторов,
обычно лучше использовать функцию.
Как правило, вместо лямбда-выражения с пустым списком захвата проще использовать функцию. Как уже упоминалось, для упорядочивания вектора по длине слов можно использовать или лямбда-выражение, или нашу функцию isShorter(). Точно так же совсем не сложно заменить лямбда-выражение, выводившее содержимое вектора, функцией, которая получает строку и выводит ее на стандартное устройство вывода.
Однако не так просто написать функцию для замены лямбда-выражений, которые захватывают локальные переменные. Рассмотрим, например, использованное в вызове функции find_if() лямбда-выражение, которое сравнивало размер строки с заданным размером. Совсем не сложно написать функцию, выполняющую те же действия: bool check_size(const string &s, string::size_type sz) { return s.size() >= sz;
}
Но мы не можем использовать эту функцию как аргумент функции find_if(). Как уже упоминалось, функция find_if() получает унарный предикат, поэтому переданное ей
Page 501/1103
вызываемое выражение должно получать один аргумент. Лямбда-выражение, переданное функцией biggies() функции find_if(), использует свой список захвата для хранения значения переменной sz. Чтобы использовать функцию check_size() вместо этого лямбда- выражения,
следует выяснить, как передать аргумент sz параметру. Библиотечная функция bind()
Проблему передачи аргумента размера функции check_size() можно решить при помощи новой библиотечной функции bind(), определенной в заголовке functional. Функцию bind()
можно считать универсальным адаптером функции (см. раздел 9.6). Она получает вызываемый объект и создает новый вызываемый объект, который адаптирует список параметров исходного объекта.
Общая форма вызова функции bind() такова: auto новыйВызываемыйОбъект = bind( вызываемыйОбъект , список_аргументов ); где новыйВызываемыйОбъект — это новый вызываемый объект, а список_аргументов — разделяемый запятыми список аргументов, соответствующих параметрам переданного вызываемого объекта вызываемыйОбъект . Таким образом, когда происходит вызов объекта новыйВызываемыйОбъект , он вызывает вызываемыйОбъект , передавая аргументы из списка список_аргументов .
Аргументы из списка список_аргументов могут включать имена в формате _ n , где n — целое число. Эти аргументы — знакоместа , представляющие параметры объекта новыйВызываемыйОбъект . Они располагаются вместо аргументов, которые будут переданы объекту новыйВызываемыйОбъект . Число n является позицией параметра вновь созданного вызываемого объекта: _1 — первый параметр, _2 — второй и т.д.Привязка параметра sz к функции check_size()
В качестве примера использования функции bind() создадим объект, который вызывает функцию check_size() с фиксированным значением ее параметра размера:
// check6 - вызываемый объект, получающий один аргумент типа string
Page 502/1103

// и вызывающий функцию check_size() с этой строкой и значением 6 auto check6 = bind(check_size, _1, 6);
У этого вызова функции bind() есть только одно знакоместо, означающее, что вызываемый объект check6() получает один аргумент. Знакоместо располагается первым в списке аргументов. Это означает, что параметр вызываемого объекта check6() соответствует первому параметру функции check_size(). Этот параметр имеет тип const string&, а значит, параметр вызываемого объекта check6() также имеет тип const string&. Таким образом, при вызове check6() следует передать аргумент типа string, который вызываемый объект check6() передаст в качестве первого аргумента функции check_size().
Второй аргумент в списке аргументов (т.е. третий аргумент функции bind()) является значением 6. Это значение связывается со вторым параметром функции check_size(). Каждый раз, когда происходит вызов вызываемого объекта check6(), он передает функции check_size() значение 6 как второй аргумент: string s = "hello"; bool b1 = check6(s); // check6(s) вызывает check_size(s, 6)
Используя функцию bind(), можно заменить следующий исходный вызов функции find_if() на базе лямбда-выражения: auto wc = find_if(words.begin(), words.end(),
[sz](const string &a) кодом, использующим функцию check_size(), auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
Этот вызов функции bind() создает вызываемый объект, который привязывает второй аргумент функции check_size() к значению параметра sz. Когда функция find_if() вызовет этот объект для строк вектора words, он, в свою очередь, вызовет функцию check_size(), передав полученную строку и значение параметра sz. Таким образом, функция find_if() фактически вызовет функцию check_size() для каждой строки в исходном диапазоне и сравнит размер этой строки со значением параметра sz. Использование имен из пространства имен placeholders
Имена формата _ n определяются в пространстве имен placeholders. Само это пространство имен определяется в пространстве имен std (см. раздел 3.1). Чтобы использовать эти имена,
следует предоставить имена обоих пространств имен. Подобно нашим другим примерам,
данные вызовы функции bind() подразумевали наличие соответствующих объявлений using.
Рассмотрим объявление using для имени _1: using std::placeholders::_1;
Это объявление свидетельствует о том, что используется имя _1, определенное в
Page 503/1103
пространстве имен placeholders, которое само определено в пространстве имен std.
Для каждого используемого имени знакоместа следует предоставить отдельное объявление using. Но поскольку написание таких объявлений может быть утомительно и ведет к ошибкам,
вместо этого можно использовать другую форму using, которая подробно рассматривается в разделе 18.2.2: using namespace пространствоимен_имя ;
Она свидетельствует, что необходимо сделать доступными для нашей программы все имена из пространства имен пространствоимен_имя : using namespace std::placeholders;
Этот код позволяет использовать все имена, определенные в пространстве имен placeholders. Подобно функции bind(), пространство имен placeholders определено в заголовке functional. Аргументы функции bind()
Как уже упоминалось, функцию bind() можно использовать для фиксированного значения параметра. В более общем случае функцию bind() можно использовать для привязки или перестройки параметров в предоставленном вызываемом объекте. Предположим, например,
что f() — вызываемый объект с пятью параметрами:
// g - вызываемый объект, получающий два аргумента auto g = bind(f, a, b, _2, с, _1);
Этот вызов функции bind() создает новый вызываемый объект, получающий два аргумента,
представленные знакоместами _2 и _1. Новый вызываемый объект передает собственные аргументы как третий и пятый аргументы вызываемому объекту f(). Первый, второй и четвертый аргументы вызываемого объекта f() связаны с переданными значениями a, b и с соответственно.
Аргументы вызываемого объекта g() связаны со знакоместами по позиции. Таким образом,
первый аргумент вызываемого объекта g() связан с параметром _1, а второй — с параметром
_2. Следовательно, когда происходит вызов g(), его первый аргумент будет передан как последний аргумент вызываемого объекта f(); второй аргумент g() будет передан как третий.
В действительности этот вызов функции bind() преобразует вызов g(_1, _2) в вызов f(а, b, _2,
с, _1).
Таким образом, вызов вызываемого объекта g() вызывает вызываемый объект f() с использованием аргументов вызываемого объекта g() для знакомест наряду с аргументами a,
b и с. Например, вызов g(X, Y) приводит к вызову f(a, b, Y, с, X). Использование функции bind()
для переупорядочивания параметров
Рассмотрим более конкретный пример применения функции bind() для переупорядочивания аргументов. Используем ее для обращения смысла функции isShorter() следующим образом:
// сортировка по длине слов от коротких к длинным
Page 504/1103
sort(words.begin(), words.end(), isShorter);
// сортировка по длине слов от длинных к коротким sort(words.begin(), words.end(), bind(isShorter, _2, _1));
В первом вызове, когда алгоритм sort() должен сравнить два элемента, А и В, он вызовет функцию isShorter(A, В). Во втором вызове аргументы функции isShorter() меняются местами.
В данном случае, когда алгоритм sort() сравнивает элементы, он вызывает функцию isShorter(В, А). Привязка ссылочных параметров
По умолчанию аргументы функции bind(), не являющиеся знакоместами, копируются в возвращаемый ею вызываемый объект. Однако, подобно лямбда-выражениям, иногда необходимо связать аргументы, которые следует передать по ссылке, или необходимо связать аргумент, тип которого не допускает копирования.
Для примера заменим лямбда-выражение, которое захватывало поток ostream по ссылке:
// os - локальная переменная, ссылающаяся на поток вывода
// с - локальная переменная типа char for_each(words.begin(), words.end(),
[&os, c] (const string &s) { os << s << c; });
Вполне можно написать функцию, выполняющую ту же задачу: ostream &print(ostream &os, const string &s, char c) { return os << s << c;
}
Но для замены захвата переменной os нельзя использовать функцию bind() непосредственно:
// ошибка: нельзя копировать os for_each(words.begin(), words.end(), bind(print, os, _1, ' '));
Поскольку функция bind() копирует свои аргументы, она не сможет скопировать поток ostream.
Если объект необходимо передать функции bind(), не копируя, то следует использовать библиотечную функцию ref(): for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
Функция ref() возвращает объект, который содержит переданную ссылку, являясь при этом вполне копируемым. Существует также функция cref(), создающая класс, содержащий ссылку
Page 505/1103
на константу. Подобно функции bind(), функции ref() и cref() определены в заголовке functional.
Совместимость с прежней версией: привязка аргументов
Прежние версии языка С++ имели много больше ограничений, и все же более сложный набор средств привязки аргументов к функциям. Библиотека определяет две функции — bind1st() и bind2nd(). Подобно функции bind(), эти функции получают функцию и создают новый вызываемый объект для вызова переданной функции с одним из ее параметров, связанным с переданным значением. Но эти функции могут связать только первый или второй параметр соответственно. Поскольку они имеют очень много ограничений, в новом стандарте эти функции не рекомендованы . Это устаревшее средство, которое может не поддерживаться в будущих выпусках. Современные программы С++ должны использовать функцию bind().Упражнения раздела 10.3.4
Упражнение 10.22. Перепишите программу подсчета слов размером 6 символов с использованием функций вместо лямбда-выражений.

1   ...   23   24   25   26   27   28   29   30   ...   54


написать администратору сайта