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


std::vector a {1.0, 2.0, 3.0};

std::vector b {4.0, 5.0, 6.0};


double sum {0};

for (size_t i {0}; i < a.size(); ++i) {

  sum += a[i] * b[i];

}

// sum = 32.0


Как же выглядит аналогичный код в языках, которые считаются более элегантными?

Haskell — чистый функциональный язык, на этом языке вычислить скалярное произведение двух векторов можно с помощью следующей волшебной строки (рис. 3.8).


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


В библиотеке STL вы можете найти специальный алгоритм:

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

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

zip
. Что она делает? Принимает два вектора
a
и
b
и преобразует их в смешанный вектор. Например, при вызове этой функции векторы
[a1, a2, a3]
и
[b1, b2, b3]
будут выглядеть как
[(a1,b1), (a2,b2), (a3,b3)]
. Посмотрите на него внимательно; он работает почти так же, как и ускорители упаковки!

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

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


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

В этом примере мы воссоздадим функцию

zip
, известную из языков Haskell и Python. Она будет работать только для векторов, содержащих значения типа
double
, чтобы не отвлекаться от механики итераторов.


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


#include 

#include 

#include 


2. Далее определим класс

zip_iterator
. При переборе диапазонов данных
zip_iterator
мы будем получать на каждом этапе пару значений из двух контейнеров. Это значит, что мы итерируем по двум контейнерам одновременно:


class zip_iterator {


3. Итератор-упаковщик должен сохранять два итератора, по одному для каждого контейнера:


  using it_type = std::vector::iterator;

  it_type it1;

  it_type it2;


4. Конструктор просто сохраняет итераторы обоих контейнеров, по которым нужно проитерировать:


public:

  zip_iterator(it_type iterator1, it_type iterator2)

    : it1{iterator1}, it2{iterator2}

  {}


5. Инкрементирование итератора-упаковщика означает инкрементирование обоих итераторов-членов:


  zip_iterator& operator++() {

    ++it1;

    ++it2;

    return *this;

  }


6. Два итератора-упаковщика считаются неравными, если оба их итератора-члена не равны своим коллегам из другого итератора-упаковщика. Обычно вы можете использовать логическое

ИЛИ (||)
вместо логического
И (&&)
, но представьте, что диапазоны данных имеют неравную длину. В таких случаях нельзя соотнести оба конечных итератора одновременно. Таким образом, можно прервать выполнение цикла при достижении первого конечного итератора в одном из диапазонов данных:


  bool operator!=(const zip_iterator& o) const {

    return it1 != o.it1 && it2 != o.it2;

  }


7. Оператор сравнения равенства реализуется с помощью другого оператора, изменяя результат его работы на противоположный:


  bool operator==(const zip_iterator& o) const {

    return !operator!=(o);

  }


8. Разыменование итератора-упаковщика открывает доступ к обоим контейнерам в одной и той же позиции:


  std::pair operator*() const {

    return {*it1, *it2};

  }

};


9. Мы рассмотрели код итератора. Нужно сделать итератор совместимым с алгоритмами STL, поэтому следует определить стереотипный код для типажа. По сути, он говорит, что данный итератор является обычным однонаправленным и при разыменовании возвращает пары значений типа

double
. Несмотря на то, что мы не использовали в текущем примере
difference_type
, для некоторых реализаций STL это может понадобиться для успешной компиляции кода:


namespace std {

  template <>

  struct iterator_traits {

    using iterator_category = std::forward_iterator_tag;

    using value_type = std::pair;

    using difference_type = long int;

  };

}


10. Следующий шаг — определение класса диапазона данных, функции

begin
и
end
которого возвращают итераторы-упаковщики:


class zipper {

  using vec_type = std::vector;

  vec_type &vec1;

  vec_type &vec2;


11. Он должен сослаться на два существующих контейнера, чтобы создать итераторы-упаковщики:


public:

  zipper(vec_type &va, vec_type &vb)

    : vec1{va}, vec2{vb}

  {}


12. Функции

begin
и
end
просто передают пары начальных и конечных указателей, чтобы создать с их помощью экземпляры итераторов-упаковщиков:


  zip_iterator begin() const {

    return {std::begin(vec1), std::begin(vec2)};

  }


  zip_iterator end() const {

    return {std::end(vec1), std::end(vec2)};

  }

};


13. Как и в примерах кода на языках Haskell и Python, определяем два вектора, содержащих значения типа

double
. Кроме того, указываем, что используем пространство имен
std
внутри функции
main
по умолчанию:


int main()

{

  using namespace std;

  vector a {1.0, 2.0, 3.0};

  vector b {4.0, 5.0, 6.0};


14. Объект класса

zipper
объединяет их в один диапазон данных, напоминающий вектор, где можно увидеть пары значений векторов
a
и
b
:


  zipper zipped {a, b};


15. Используем метод

std::accumulate
, чтобы сложить все элементы диапазона данных. Сделать это напрямую нельзя, поскольку в результате мы сложим экземпляры типа
std::pair
, для которых концепция суммирования не определена. Поэтому зададим вспомогательное лямбда-выражение, которое принимает пару, перемножает ее члены и складывает результат со значением переменной-аккумулятора. Функция
std::accumulate
хорошо работает с лямбда-выражениями со следующей сигнатурой:


  const auto add_product ([](double sum, const auto &p) {

    return sum + p.first * p.second;

  }
);


16. Теперь передадим его функции

std::accumulate
, а также пару итераторов для упаковываемых диапазонов и стартовое значение