Предположим, нужно написать функцию-наблюдатель для какого-то значения, которое может изменяться время от времени, что приведет к оповещению других объектов, например индикатора давления газа, цены на акцию т.п. При изменении значения должен вызываться список объектов-наблюдателей, которые затем по-своему на это отреагируют.
Для реализации задачи можно поместить несколько объектов функции-наблюдателя в вектор, все они будут принимать в качестве параметра переменную типа int
, которая представляет наблюдаемое значение. Мы не знаем, что именно станут делать данные функции при вызове, но нам это и неинтересно.
Какой тип будут иметь объекты функций, помещенные в вектор? Нам подойдет тип std::vector
, если мы захватываем указатели на void f(int);
. Данный тип сработает с любым лямбда-выражением, которое захватывает нечто, имеющее
Не стоит указывать пользователю, что он может сохранить объекты функции наблюдателя, которые ничего не захватывают, поскольку это ограничивает варианты применения. Как же позволить ему сохранять любые объекты функций, ограничивая лишь интерфейс вызова, принимающий конкретный диапазон параметров в виде наблюдаемых значений?
В этом разделе мы рассмотрим способ решения данной проблемы с помощью объекта std::function
, который может выступать в роли полиморфической оболочки для любого лямбда-выражения, независимо от того, какие значения оно захватывает.
Как это делается
В этом примере мы создадим несколько лямбда-выражений, значительно отличающихся друг от друга, но имеющих одинаковую сигнатуру вызова. Затем сохраним их в одном векторе с помощью std::function
.
1. Сначала включим необходимые заголовочные файлы:
#include
#include
#include
#include
#include
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
std::list
std::vector
5. Сейчас воспользуемся функцией consumer для работы с нашими экземплярами контейнеров d
, l
и v:
создадим для них объекты-потребители функций и поместим их в экземпляр vector
. Эти объекты функций будут захватывать ссылку на один из объектов контейнера. Последние имеют разные типы, как и объекты функций. Тем не менее вектор хранит экземпляры типа std::function
. Все объекты функций неявно оборачиваются в объекты типа std::function
, которые затем сохраняются в векторе:
const std::vector
{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