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

разные переменные, также делает их типы совершенно разными. Это похоже на попытку разместить в векторе переменные типов 
A
,
B
и
C
, когда сами типы не имеют ничего общего.

Чтобы это исправить, нужно найти общий тип, способный хранить разные объекты функций, например

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

Представьте, будто мы написали такой код:


std::function f (

  [&vector](int x) { vector.push_back(x); });


Здесь объект функции, создаваемый на основе лямбда-выражения, обернут в объект типа

std::function
, и всякий раз вызов
f(123)
приводит к виртуальному вызову функции, который перенаправляется реальному объекту функции, находящемуся внутри.

При сохранении объектов функции экземпляры

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


 Многие программисты-новички думают или надеются, что

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

Создаем функции методом конкатенации

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

unique_words
, а во второй показывается ее использование на примере строки (рис. 4.2).


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

unique_words
, в которой к входным данным применяется набор функций. Сначала все символы преобразуются в строчные с помощью
map toLower
. Таким образом, слова наподобие FOO и foo могут считаться одним словом. Далее функция
words
разбивает предложение на отдельные слова. Например, из строки
"foo bar baz"
мы получим массив
["foo", "bar", "baz"]
. Следующий шаг — сортировка нового списка слов. В результате последовательность слов
["a", "b", "a"]
будет выглядеть как
["a", "a", "b"]
. Теперь в дело вступает функция
group
. Она группирует последовательные слова в списки, т.е. конструкция
["a", "a", "b"]
получит вид
[["a", "a"], ["b"]]
. Задача практически выполнена, и теперь нужно сосчитать, сколько получилось групп одинаковых слов. В этом поможет функция
length
.

Это замечательный стиль программирования, так как мы можем прочитать код справа налево, поскольку, по сути, описываем процесс преобразования предложения. Нет нужды знать, как реализованы отдельные фрагменты (если только они не будут работать слишком медленно и с ошибками).

Однако мы здесь не ради восхваления Haskell, а чтобы улучшить навыки работы с языком С++. Он позволяет написать аналогичный код. Мы не сможем достичь того же уровня элегантности, какой видели в языке Haskell, зато у нас под рукой самый быстрый язык программирования. В этом разделе показано, как инициировать конкатенацию функций в С++ с помощью лямбда-выражений.


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

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


1. Сначала включим несколько заголовочных файлов:


#include 

#include 


2. Далее реализуем вспомогательную функцию

concat
, которая принимает множество параметров. Таковыми выступят функции наподобие
f
,
g
и
h
, а результатом будет еще один объект функции, применяющий функции
f(g(h(...)))
для любых входных данных.


template 

auto concat(T t, Ts ...ts)

{


3. Теперь задача усложняется. Когда пользователь предоставит функции

f
,
g
и
h
, мы оценим это выражение как
f(concat(g, h))
, которое будет распаковано в
f(g(concat(h)))
, на чем рекурсия остановится, и мы получим выражение
f(g(h(...)))
. Данная цепочка вызовов функций, представляющая конкатенацию пользовательских функций, захватывается лямбда-выражением, которое затем может принять какие-то параметры
p
и передать в вызов
f(g(h(p)))
. Мы будем возвращать это лямбда-выражение. Конструкция
if constexpr
проверяет, находимся ли мы на шаге рекурсии, требующем сконкатенировать более чем одну функцию:


  if constexpr (sizeof...(ts) > 0) {

    return [=](auto ...parameters) {

      return t(concat(ts...)(parameters...));

    };

  }


4. Еще одна ветвь конструкции

if constexpr
будет выбрана компилятором в том случае, если достигнут конец рекурсии. В таких ситуациях просто возвращаем функцию,
t
, поскольку она является единственным оставшимся параметром:


  else {

    return t;

  }

}


5. Теперь применим нашу новую функцию конкатенации, передав в нее несколько функций. Начнем с функции

main
, где определим два дешевых объекта функций:


int main()

{

  auto twice ([] (int i) { return i * 2; });

  auto thrice ([] (int i) { return i * 3; });


6. Выполним конкатенацию. Объединим два объекта функций умножения с помощью функции STL

std::plus
, которая принимает два параметра и возвращает их сумму. Таким образом, получим функцию, выполняющую вызов
twice(thrice(plus(a, b )))
.


  auto combined (

    concat(twice, thrice, std::plus{})

  );


7. Воспользуемся тем, что получилось. Функция

combined
теперь выглядит как обычная, и компилятор может объединять эти функции без особых задержек:


  std::cout << combined(2, 3) << '\n';

}


8. Компиляция и запуск программы дадут следующий результат, и он не будет неожиданным, поскольку

2*3*(2+3)
равно
30
:


$ ./concatenation

30


Как это работает

Самой сложной частью этого раздела является функция

concat
. Она выглядит очень мудреной, поскольку разворачивает набор параметров
ts
в другое лямбда-выражение, которое рекурсивно снова вызывает функцию
concat
, теперь уже с меньшим количеством параметров:


template 

auto concat(T t, Ts ts)

{

  if constexpr (sizeof...(ts) > 0) {

    return [=](auto ...parameters) {

      return t(concat(ts...)(parameters...));

    };

  } else {

    return [=](auto ...parameters) {