C++17 STL Стандартная библиотека шаблонов — страница 38 из 119

правильные объекты функций.


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);