Шрифт:
Интервал:
Закладка:
В этом разделе мы рассмотрим способ решения данной проблемы с помощью объекта std::function, который может выступать в роли полиморфической оболочки для любого лямбда-выражения, независимо от того, какие значения оно захватывает.
Как это делается
В этом примере мы создадим несколько лямбда-выражений, значительно отличающихся друг от друга, но имеющих одинаковую сигнатуру вызова. Затем сохраним их в одном векторе с помощью std::function.
1. Сначала включим необходимые заголовочные файлы:
#include <iostream>
#include <deque>
#include <list>
#include <vector>
#include <functional>
2. Реализуем небольшую функцию, которая возвращает лямбда-выражение. Она принимает контейнер и возвращает объект функции, захватывающий этот контейнер по ссылке. Сам по себе объект функции принимает целочисленный параметр. Когда данный объект получает целое число, он добавит его в свой контейнер.
static auto consumer (auto &container){
return [&] (auto value) {
container.push_back(value);
};
}
3. Еще одна небольшая вспомогательная функция выведет на экран содержимое экземпляра контейнера, который мы предоставим в качестве параметра:
static void print (const auto &c)
{
for (auto i : c) {
std::cout << i << ", ";
}
std::cout << 'n';
}
4. В функции main мы создадим объекты классов deque, list и vector, каждый из которых будет хранить целые числа:
int main()
{
std::deque<int> d;
std::list<int> l;
std::vector<int> v;
5. Сейчас воспользуемся функцией consumer для работы с нашими экземплярами контейнеров d, l и v: создадим для них объекты-потребители функций и поместим их в экземпляр vector. Эти объекты функций будут захватывать ссылку на один из объектов контейнера. Последние имеют разные типы, как и объекты функций. Тем не менее вектор хранит экземпляры типа std::function<void(int)>. Все объекты функций неявно оборачиваются в объекты типа std::function, которые затем сохраняются в векторе:
const std::vector<std::function<void(int)>> consumers
{consumer(d), consumer(l), consumer(v)};
6. Теперь поместим десять целочисленных значений во все структуры данных, проходя по значениям в цикле, а затем пройдем в цикле по объектам функций-потребителей, которые вызовем с записанными значениями:
for (size_t i {0}; i < 10; ++i) {
for (auto &&consume : consumers) {
consume(i);
}
}
7. Все три контейнера теперь должны содержать одинаковые десять чисел. Выведем на экран их содержимое:
print(d);
print(l);
print(v);
}
8. Компиляция и запуск программы дадут следующий результат, который выглядит именно так, как мы и ожидали:
$ ./std_function
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
Как это работает
Самой сложной частью этого примера является следующая строка:
const std::vector<std::function<void(int)>> consumers
{consumer(d), consumer(l), consumer(v)};
Объекты d, l и v обернуты в вызов consumer(...). Он возвращает объекты функций, каждый из которых захватывает ссылки на один из объектов — d, l или v. Хотя все объекты функций принимают в качестве параметров целочисленные значения, тот факт, что они захватывают абсолютно разные переменные, также делает их типы совершенно разными. Это похоже на попытку разместить в векторе переменные типов A, B и C, когда сами типы не имеют ничего общего.
Чтобы это исправить, нужно найти общий тип, способный хранить разные объекты функций, например std::function. Объект типа std::function<void(int)> может хранить любой объект функции или традиционную функцию, которая принимает целочисленный параметр и ничего не возвращает. С помощью полиморфизма он отвязывает тип от лежащего в его основе типа объекта функции.
Представьте, будто мы написали такой код:
std::function<void(int)> f (
[&vector](int x) { vector.push_back(x); });
Здесь объект функции, создаваемый на основе лямбда-выражения, обернут в объект типа std::function, и всякий раз вызов f(123) приводит к виртуальному вызову функции, который перенаправляется реальному объекту функции, находящемуся внутри.
При сохранении объектов функции экземпляры std::function применяют некоторую логику. При захвате все большего и большего количества переменных лямбда-выражение будет увеличиваться. Если его размер относительно мал, то std::function может хранить его внутри себя. Если же размер сохраненного объекта функций слишком велик, то std::function выделит фрагмент памяти в куче и сохранит объект там. Возможности нашего кода в подобном случае затронуты не будут, но знать об этом нужно, поскольку такая ситуация может повлиять на его производительность.
Многие программисты-новички думают или надеются, что std::function<...> на самом деле выражает тип лямбда-выражения. Отнюдь. Это полиморфическая вспомогательная библиотечная функция, которая полезна для оборачивания лямбда-выражений и сокрытия различий в их типах.
Создаем функции методом конкатенации
Многие проблемы можно решить, не полагаясь исключительно на собственный код. Например, взглянем на то, как решается задача поиска уникальных слов в тексте на языке программирования Haskell. В первой строке определяется функция unique_words, а во второй показывается ее использование на примере строки (рис. 4.2).
Ого! Программа получилась действительно короткой. Не вдаваясь особо в синтаксис языка Haskell, взглянем на то, что делает код. Определяется функция unique_words, в которой к входным данным применяется набор функций. Сначала все символы преобразуются в строчные с помощью map toLower. Таким образом, слова наподобие FOO и foo могут считаться одним словом. Далее функция words разбивает предложение на отдельные слова. Например, из строки "foo bar baz" мы получим массив ["foo", "bar", "baz"]. Следующий шаг — сортировка нового списка слов. В результате последовательность слов ["a", "b", "a"] будет выглядеть как ["a", "a", "b"]. Теперь в дело вступает функция group. Она группирует последовательные слова в списки, т.е. конструкция ["a", "a", "b"] получит вид [["a", "a"], ["b"]]. Задача практически выполнена, и теперь нужно сосчитать, сколько получилось групп одинаковых слов. В этом поможет функция length.
Это замечательный стиль программирования,