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

, а затем вызов
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