📚 Hub Books: Онлайн-чтение книгРазная литератураC++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

Шрифт:

-
+

Интервал:

-
+
1 ... 35 36 37 38 39 40 41 42 43 ... 121
Перейти на страницу:
...xs) {

  (void)std::initializer_list<int>{

    ((void)f(xs), 0)...

  };

});

Сердцем данной функции является выражение f(xs).xs — набор параметров, и нужно распаковать его, чтобы получить отдельные значения и передать их отдельным вызовам функции f. К сожалению, мы не можем просто написать конструкцию f(xs)... с помощью нотации ..., с которой уже знакомы.

Вместо этого можно создать список значений с помощью std::initializer_list, имеющего конструктор с переменным числом параметров. Выражение наподобие return std::initializer_list<int>{f(xs)...}; решает задачу, но имеет недостатки. Взглянем на реализацию функции for_each, которая тоже работает и при этом выглядит проще нашего варианта:

auto for_each ([](auto f, auto ...xs) {

  return std::initializer_list<int>{f(xs)...};

});

Она более проста для понимания, но имеет следующие недостатки.

1. Создает список инициализаторов для возвращаемых значений на основе вызовов функции f. К этому моменту нас не волнуют возвращаемые значения.

2. Возвращает данный список, а нам нужна функция, работающая в стиле «запустил и забыл», которая не возвращает ничего.

3. Вполне возможно, что f — функция, которая не возвращает ничего, в таком случае код даже не будет скомпилирован.

Гораздо более сложная функция for_each решает все эти проблемы. Она делает следующее.

1. Не возвращает список инициализаторов, а приводит все выражение к типу void с помощью (void)std::initializer_list<int>{...}.

2. Внутри инициализирующего выражения преобразует выражение f(xs)... в выражение (f(xs),0). Это приводит к тому, что возвращаемое выражение отбрасывается, а значение 0 все еще помещается в список инициализаторов.

3. Конструкция f(xs) в выражении (f(xs),0).  также преобразуется к типу void, поэтому возвращаемое значение, если таковое существует, нигде не обрабатывается.

Объединение этих особенностей, к сожалению, ведет к появлению уродливой конструкции, но она корректно работает и компилируется для множества объектов функций независимо от того, возвращают ли они какое-то значение.

Приятной особенностью описанного механизма является тот факт, что порядок вызовов функций будет сохраняться в строгой последовательности.

 

 Выполнять преобразование конструкции (void)выражение в рамках старой нотации языка С не рекомендуется, поскольку в языке С++ имеются собственные операции преобразования. Вместо этого стоит использовать конструкцию reinterpret_cast<void>(выражение), но данный вариант еще больше снизит удобочитаемость кода.

Реализуем функцию transform_if с применением std::accumulate и лямбда-выражений

Большинство разработчиков, применяющих std::copy_if и std::transform, могли задаваться вопросом, почему не существует функции std::transform_if. Функция std::copy_if копирует элементы из исходного диапазона по месту назначения, но опускает элементы, не соответствующие определенной пользователем функции-предикату. Функция std::transform безусловно копирует все элементы из исходного диапазона по месту назначения, но при этом преобразует их в процессе. Это происходит с помощью функции, которая определена пользователем и может выполнять как нечто простое (например, умножение чисел), так и полные преобразования к другим типам.

Эти функции существуют достаточно давно, но функции std::transform_if все еще нет. Ее можно легко создать, реализовав функцию, которая итерирует по диапазонам данных и копирует все элементы, соответствующие предикату, выполняя в процессе их преобразование. Однако мы воспользуемся случаем и разберем решение данной задачи с точки зрения лямбда-выражений.

Как это делается

В этом примере мы создадим собственную функцию transform_if, которая работает, передавая алгоритму std::accumulate правильные объекты функций.

1. Как и всегда, включим некоторые заголовочные файлы:

#include <iostream>

#include <iterator>

#include <numeric>

2. Сначала реализуем функцию с именем map. Она принимает функцию преобразования входных данных и возвращает объект функции, который будет работать с функцией std::accumulate:

template <typename T>

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 <typename T>

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<int> it {std::cin};

  std::istream_iterator<int> 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, который

1 ... 35 36 37 38 39 40 41 42 43 ... 121
Перейти на страницу:

Комментарии

Обратите внимание, что комментарий должен быть не короче 20 символов. Покажите уважение к себе и другим пользователям!

Никто еще не прокомментировал. Хотите быть первым, кто выскажется?