правильные объекты функций.
1. Как и всегда, включим некоторые заголовочные файлы:
#include
#include
#include
2. Сначала реализуем функцию с именем
map
. Она принимает функцию преобразования входных данных и возвращает объект функции, который будет работать с функцией std::accumulate
:
template
auto map(T fn)
{
3. Мы будем возвращать объект функции, принимающий функцию
reduce
. Когда данный объект вызывается с этой функцией, он возвращает другой объект функции, который принимает аккумулятор и входной параметр. Он вызывает функцию reduce
для этого аккумулятора и преобразованной входной переменной fn
. Если это описание кажется вам слишком сложным — не волнуйтесь, далее мы соберем все вместе и посмотрим, как работают эти функции.
return [=] (auto reduce_fn) {
return [=] (auto accum, auto input) {
return reduce_fn(accum, fn(input));
};
};
}
4. Теперь реализуем функцию
filter
. Она работает точно так же, как и функция map
, но не затрагивает входные данные, в то время как map
преобразует их с помощью функции transform
. Вместо этого принимаем функцию-предикат и опускаем те входные переменные, которые не соответствуют данному предикату, не выполняя для них функцию reduce
.
template
auto filter(T predicate)
{
5. Два лямбда-выражения имеют такие же сигнатуры функций, что и выражения в функции
map
. Единственное отличие заключается в следующем: входной параметр остается неизменным. Функция-предикат используется для определения того, будем ли мы вызывать функцию reduce_fn
для входных данных или же получим доступ к аккумулятору, не внося никаких изменений.
return [=] (auto reduce_fn) {
return [=] (auto accum, auto input) {
if (predicate(input)) {
return reduce_fn(accum, input);
} else {
return accum;
}
};
};
}
6. Теперь воспользуемся этими вспомогательными функциями. Создадим экземпляры итераторов, которые позволяют считать целочисленные значения из стандартного потока ввода:
int main()
{
std::istream_iterator it {std::cin};
std::istream_iterator end_it;
7. Далее определим функцию-предикат
even
, которая возвращает значение true
, если перед нами четное число. Функция преобразования twice
умножает свой целочисленный параметр на 2
:
auto even ([](int i) { return i % 2 == 0; });
auto twice ([](int i) { return i * 2; });
8. Функция
std::accumulate
принимает диапазон значений и аккумулирует их. Аккумулирование по умолчанию означает суммирование значений с помощью оператора +
. Мы хотим предоставить собственную функцию аккумулирования. Таким образом, хранить сумму значений не нужно. Мы присвоим каждое значение из диапазона разыменованному итератору it
, а затем вернем его после продвижения вперед.
auto copy_and_advance ([](auto it, auto input) {
*it = input; return ++it;
});
9. Наконец мы готовы собрать все воедино. Мы итерируем по стандартному потоку ввода и предоставляем вывод
ostream_iterator
, который выдает значения в консоль. Объект функции copy_and_advance
работает с этим итератором вывода, присваивая ему целые числа, полученные от пользователя. По сути, данное действие выводит на экран присвоенные элементы. Но мы хотим видеть только четные числа, полученные от пользователя, и умножить их. Для этого оборачиваем функцию copy_and_advance
в фильтр even, а затем — в преобразовательtwice
.
std::accumulate(it, end_it,
std::ostream_iterator{std::cout, ", "},
filter(even)(
map(twice)(
copy_and_advance
)
));
std::cout << '\n';
}
10. Компиляция и запуск программы дадут следующий результат. Значения
1
, 3
и 5
отбрасываются, поскольку являются нечетными, а 2
, 4
и 6
выводятся на экран после их умножения на два.
$ echo "1 2 3 4 5 6" | ./transform_if
4, 8, 12,
Как это работает
Этот пример выглядит довольно сложным, поскольку мы активно использовали вложенные лямбда-выражения. Чтобы понять, как все работает, взглянем на внутреннее содержимое функции
std::accumulate
. Именно так она выглядит в обычной реализации, предлагаемой в библиотеке STL:
template
T accumulate(InputIterator first, InputIterator last, T init, F f)
{
for (; first != last; ++first) {
init = f(init, *first);
}
return init;
}
Параметр функции
f
выполняет здесь остальную работу, а цикл собирает результаты в предоставленной пользователем переменной init
. В обычном варианте использования диапазон итераторов может представлять собой вектор чисел, например 0
, 1
, 2
, 3
, 4
, а переменная init
будет иметь значение 0
. Функция f
является простой бинарной функцией, которая может определять сумму двух элементов с помощью оператора +
.В нашем примере цикл просто складывает все элементы и записывает результат в переменную
init
, это выглядит, например, так: init = (((0+1)+2)+3)+4
. Подобная запись помогает понять, что std::accumulate
представляет собой функцию свертки. Выполнение свертки для диапазона значений означает применение бинарной операции для переменной-аккумулятора и каждого элемента диапазона пошагово (результат каждой операции является значением-аккумулятором для последующей операции). Поскольку эта функция обобщена, с ее помощью можно решать разные задачи, например реализовать функцию std::transform_if
! Функция f
в таком случае будет называться функцией reduce (свертки).Очень прямолинейная реализация функции
transform_if
будет выглядеть так:
template
typename P, typename Transform>
OutputIterator transform_if(InputIterator first, InputIterator last,
OutputIterator out,
P predicate, Transform trans)
{
for (; first != last; ++first) {
if (predicate(*first)) {
*out = trans(*first);