Шрифт:
Интервал:
Закладка:
(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, который