10.3.2. Лямбда-выражения
У передаваемых алгоритмам предикатов должен быть точно один или два параметра, в зависимости от того, использует ли алгоритм унарный или бинарный предикат соответственно. Но иногда необходима обработка, которая требует большего количества аргументов, чем позволяет предикат алгоритма. Например, решение для последнего упражнения в предыдущем разделе имело жестко заданный размер в 5 символов, согласно которому предикат делил последовательность. Было бы удобней иметь возможность разделять последовательность без необходимости писать отдельный предикат для каждого возможного размера.
Для примера пересмотрим программу из раздела 10.3.1 так, чтобы вывести количество слов с указанным размером или больше него. Вывод изменим так, чтобы он сообщал только те слова, длина которых равна или больше заданной.
Вот "эскиз" этой функции, которую мы назовем biggies():
void biggies(vector<string> &words,
vector<string>::size_type sz) {
elimDups(words); // расположить слова в алфавитном порядке
// и удалить дубликаты
// пересортировать по длине, поддерживая алфавитный порядок среди слов
// той же длины
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). Таким образом, если е — вызываемое выражение, то можно написать е(аргументы), где аргументы — это разделяемый запятыми список любого количества аргументов.
Единственными вызываемыми объектами, использованными до сих пор, были функции и указатели на функции (см. раздел 6.7). Есть еще два вида вызываемых объектов: классы, перегружающие оператор вызова функции (будут рассматриваться в разделе 14.8), и лямбда-выражения (lambda expression).
[список захвата](список параметров) -> тип возвращаемого значения
{тело функции}
где список захвата (зачастую пустой) — это список локальных переменных, определенных в содержащей функции; тип возвращаемого значения, список параметров и тело функции — те же самые, что и у любой обычной функции. Однако, в отличие от обычных функций, для определения типа возвращаемого значения лямбда-выражения должны использовать замыкающий тип (см. раздел 6.3.3).
Список параметров и типа возвращаемого значения могут отсутствовать, но список захвата и тело функции должны быть всегда:
auto f = [] { return 42; };
Здесь f определено как вызываемый объект, не получающий никаких аргументов и возвращающий значение 42. Вызов лямбда-выражений происходит таким же способом, что и вызов функций, — при помощи оператора вызова:
cout << f() << endl; // выводит 42
Пропуск круглых скобок и списка параметров в лямбда-выражении эквивалентен определению пустого списка параметров. Следовательно, когда происходит вызов лямбда-выражения f, список аргументов оказывается пустым. Если пропущен тип возвращаемого значения, то выведенный тип возвращаемого значения лямбда-выражения будет зависеть от кода в теле функции. Если телом является только оператор 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().
Хотя лямбда-выражение может присутствовать в функции, оно способно использовать локальные переменные этой функции, только заранее определив, какие из них предстоит использовать. Лямбда-выражение определяет подлежащие использованию локальные переменные, включив их в список захвата (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;
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); // расположить слова в алфавитном порядке
// и удалить дубликаты
// пересортировать по длине, поддерживая алфавитный порядок среди слов
// той же длины
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.
Упражнение 10.19. Перепишите предыдущее упражнение так, чтобы использовать алгоритм stable_partition(), который, подобно алгоритму stable_sort(), обеспечивает исходный порядок элементов в разделяемой последовательности.