, а затем вызов call_all(123)
приведет к серии вызовов f(123); g(123); h(123);
. Эта функция уже выглядит сложной, поскольку требуется распаковать набор параметров, в котором находятся функции, в набор вызовов с помощью конструктора std::initializer_list
.
static auto multicall (auto ...functions)
{
return [=](auto x) {
(void)std::initializer_list{
((void)functions(x), 0)...
};
};
}
3. Следующая вспомогательная функция принимает функцию
f
и набор параметров xs
. После этого она вызывает функцию f
для каждого из параметров. Таким образом, вызов for_each(f,1,2,3)
приводит к серии вызовов: f(1); f(2); f(3);
. Функция, по сути, использует такой же синтаксический прием для распаковки набора параметров xs
в набор вызовов функций, как и функция, показанная ранее.
static auto for_each (auto f, auto ...xs) {
(void)std::initializer_list{
((void)f(xs), 0)...
};
}
4. Функция
brace_print
принимает два символа и возвращает новый объект функции, принимающий один параметр x
. Она выводит его на экран, окружив двумя символами, которые мы только что захватили.
static auto brace_print (char a, char b) {
return [=] (auto x) {
std::cout << a << x << b << ", ";
};
}
5. Теперь наконец можно использовать все эти функции в функции
main
. Сначала определим функции f
, g
и h
. Они представляют функции print
, которые принимают значения и выводят их на экран, окружив разными скобками. Функция nl
принимает любой параметр и просто выводит на экран символ переноса строки.
int main()
{
auto f (brace_print('(', ')'));
auto g (brace_print('[', ']'));
auto h (brace_print('{', '}'));
auto nl ([](auto) { std::cout << '\n'; });
6. Объединим все эти функции с помощью вспомогательной функции
multicall
:
auto call_fgh (multicall(f, g, h, nl));
7. Мы хотим, чтобы каждое предоставленное нами число было выведено на экран трижды в разных скобках. Таким образом, нужно выполнить один вызов функции, который приведет к пяти вызовам нашей мультифункции, а та, в свою очередь, выполнит четыре вызова для функций
f
, g
, h
и nl
:
for_each(call_fgh, 1, 2, 3, 4, 5);
}
8. Перед компиляцией и запуском программы подумаем, какой результат должны получить:
$ ./multicaller
(1), [1], {1},
(2), [2], {2},
(3), [3], {3},
(4), [4], {4},
(5), [5], {5},
Как это работает
Вспомогательные функции, которые мы только что реализовали, выглядят очень сложными. Так произошло из-за распаковки набора параметров с помощью
std::initializer_list
. Почему мы вообще использовали эту структуру данных? Еще раз взглянем на for_each
:
auto for_each ([](auto f, auto ...xs) {
(void)std::initializer_list{
((void)f(xs), 0)...
};
});
Сердцем данной функции является выражение
f(xs).xs
— набор параметров, и нужно распаковать его, чтобы получить отдельные значения и передать их отдельным вызовам функции f
. К сожалению, мы не можем просто написать конструкцию f(xs)...
с помощью нотации ...
, с которой уже знакомы.Вместо этого можно создать список значений с помощью
std::initializer_list
, имеющего конструктор с переменным числом параметров. Выражение наподобие return std::initializer_list{f(xs)...};
решает задачу, но имеет недостатки. Взглянем на реализацию функции for_each
, которая тоже работает и при этом выглядит проще нашего варианта:
auto for_each ([](auto f, auto ...xs) {
return std::initializer_list{f(xs)...};
});
Она более проста для понимания, но имеет следующие недостатки.
1. Создает список инициализаторов для возвращаемых значений на основе вызовов функции
f
. К этому моменту нас не волнуют возвращаемые значения.2. Возвращает данный список, а нам нужна функция, работающая в стиле «запустил и забыл», которая не возвращает ничего.
3. Вполне возможно, что
f
— функция, которая не возвращает ничего, в таком случае код даже не будет скомпилирован.
Гораздо более сложная функция
for_each
решает все эти проблемы. Она делает следующее.1. Не возвращает список инициализаторов, а приводит все выражение к типу
void
с помощью (void)std::initializer_list{...}
.2. Внутри инициализирующего выражения преобразует выражение
f(xs)...
в выражение (f(xs),0)
. Это приводит к тому, что возвращаемое выражение отбрасывается, а значение 0
все еще помещается в список инициализаторов.3. Конструкция
f(xs)
в выражении (f(xs),0)
. также преобразуется к типу void
, поэтому возвращаемое значение, если таковое существует, нигде не обрабатывается.
Объединение этих особенностей, к сожалению, ведет к появлению уродливой конструкции, но она корректно работает и компилируется для множества объектов функций независимо от того, возвращают ли они какое-то значение.
Приятной особенностью описанного механизма является тот факт, что порядок вызовов функций будет сохраняться в строгой последовательности.
Выполнять преобразование конструкции
(void)выражение
в рамках старой нотации языка С не рекомендуется, поскольку в языке С++ имеются собственные операции преобразования. Вместо этого стоит использовать конструкцию reinterpret_cast(выражение)
, но данный вариант еще больше снизит удобочитаемость кода.Реализуем функцию transform_if с применением std::accumulate и лямбда-выражений
Большинство разработчиков, применяющих
std::copy_if
и std::transform
, могли задаваться вопросом, почему не существует функции std::transform_if
. Функция std::copy_if
копирует элементы из исходного диапазона по месту назначения, но опускает элементы, не соответствующие определенной пользователем функции-предикату. Функция std::transform
безусловно копирует все элементы из исходного диапазона по месту назначения, но при этом преобразует их в процессе. Это происходит с помощью функции, которая определена пользователем и может выполнять как нечто простое (например, умножение чисел), так и полные преобразования к другим типам.Эти функции существуют достаточно давно, но функции
std::transform_if
все еще нет. Ее можно легко создать, реализовав функцию, которая итерирует по диапазонам данных и копирует все элементы, соответствующие предикату, выполняя в процессе их преобразование. Однако мы воспользуемся случаем и разберем решение данной задачи с точки зрения лямбда-выражений.
Как это делается
В этом примере мы создадим собственную функцию
transform_if
, которая работает, передавая алгоритму std::accumulate