Шрифт:
Интервал:
Закладка:
vector<int> v (w * h);
iota(begin(v), end(v), 0);
transform(begin(v), end(v), begin(v), to_iteration_count);
10. На этом, по сути, все. Теперь у нас есть вектор v, который мы инициализировали одномерными координатами и переписали счетчиком итераций для множества Мандельброта. На его основе можно вывести красивое изображение. Можно сделать окно консоли длиной w символов, чтобы не выводить на экран символ перевода строки. Но мы можем также нестандартно использовать алгоритм std::accumulate, чтобы он добавил разрывы строк за нас. Он применяет бинарную функцию для сокращения диапазона. Предоставим ему бинарную функцию, принимающую итератор вывода (который мы свяжем с терминалом на следующем шаге) и отдельное значение из диапазона. Выведем это значение как символ *, если количество итераций превышает 50. В противном случае выведем пробел. При нахождении в конце строки (поскольку переменная-счетчик n без остатка делится на w) выведем символ разрыва строки:
auto binfunc ([w, n{0}] (auto output_it, int x) mutable {
*++output_it = (x > 50 ? '*' : ' ');
if (++n % w == 0) { ++output_it = 'n'; }
return output_it;
});
11. Вызывая функцию std::accumulate для входного диапазона данных вместе с нашей бинарной функцией print и итератором ostream_iterator, можно отправить рассчитанное множество Мандельброта в окно консоли:
accumulate(begin(v), end(v), ostream_iterator<char>{cout},
binfunc);
}
12. Компиляция и запуск программы приводят к следующему результату, который выглядит как изначальное детализированное множество Мандельброта в упрощенной форме (рис. 6.9).
Как это работает
Все расчеты происходят во время вызова std::transform для одномерного массива:
vector<int> v (w * h);
iota(begin(v), end(v), 0);
transform(begin(v), end(v), begin(v), to_iteration_count);
Что же произошло и почему это работает именно так? Функция to_iteration_count, по сути, представляет собой цепочку вызовов от i_to_xy до scale и mandelbrot_iterations. На рис. 6.10 показаны этапы преобразования.
Подобным способом в качестве входного параметра можно использовать индекс одномерного массива и получать количество итераций множества Мандельброта для точки двумерной плоскости, которую представляет точка массива. К счастью, эти три преобразования совершенно не взаимозависимы. Модули подобного кода можно легко протестировать отдельно друг от друга. Таким образом, находить и исправлять ошибки очень легко, как и просто утверждать о корректности кода.
Создаем собственный алгоритм split
В некоторых ситуациях существующих алгоритмов STL недостаточно. Но ничто не запрещает нам реализовать собственный алгоритм. Прежде чем решать конкретную задачу, следует тщательно ее обдумать, чтобы понять: многие задачи можно решить путем обобщения. Если мы будем регулярно добавлять в наши библиотеки новый код по мере решения собственных задач, то можем помочь коллегам-программистам, у которых появятся аналогичные задачи. Идея заключается в том, чтобы знать, когда ваш код является достаточно обобщенным и когда не нужно обобщать его еще больше, в противном случае у нас получится новый язык общего назначения.
В этом примере мы реализуем алгоритм, который назовем split. Он может разбить любой диапазон данных, используя в качестве разделителя каждое включение конкретного значения, и скопировать полученный результат в выходной диапазон данных.
Как это делается
В данном примере мы реализуем собственный алгоритм split и проверим его работу, разбив на фрагменты строку-пример.
1. Сначала включим некоторые части библиотеки STL и объявим об использовании пространства имен std:
#include <iostream>
#include <string>
#include <algorithm>
#include <iterator>
#include <list>
using namespace std;
2. Алгоритм, показанный в этом разделе, предназначен для разбиения диапазонов данных. Он принимает начальный и конечный итераторы, а также итератор вывода, что поначалу делает его похожим на алгоритмы std::copy и std::transform. Другими его параметрами являются split_val и bin_func. Параметр split_val — значение, которое мы ищем во входном диапазоне данных; оно представляет собой точку разбиения, по которой мы отделяем входной диапазон данных. Параметр bin_func — функция, преобразующая пару итераторов, которые отмечают начало и конец этого поддиапазона. Мы проитерируем по входному диапазону данных с помощью std::find, так что будем перескакивать между включениями значений split_val. При разбиении отдельной строки на отдельные слова мы станем перескакивать от пробела к пробелу. При нахождении искомого значения останавливаемся, чтобы сформировать фрагмент и передать его в выходной диапазон данных:
template <typename InIt, typename OutIt, typename T, typename F>
InIt split(InIt it, InIt end_it, OutIt out_it, T split_val,
F bin_func)
{
while (it != end_it) {
auto slice_end (find(it, end_it, split_val));
*out_it++ = bin_func(it, slice_end);
if (slice_end == end_it) { return end_it; }
it = next(slice_end);
}
return it;
}
3. Воспользуемся новым алгоритмом. Создадим строку, которую затем разобьем на части. Элементом, отмечающим конец последнего фрагмента и начало следующего, будет дефис '-':
int main()
{
const string s {"a-b-c-d-e-f-g"};
4. Когда алгоритм вызывает функцию bin_func для пары итераторов, мы хотим создать на его основе новую строку:
auto binfunc ([](auto it_a, auto it_b) {
return string(it_a, it_b);
});
5. Выходной диапазон данных представляет собой список строк. Мы вызовем алгоритм split, который спроектирован так, что похож на другие алгоритмы STL:
list<string> l;
split(begin(s), end(s), back_inserter(l), '-', binfunc);
6. Чтобы увидеть полученное, выведем на экран новый список разбитых строк:
copy(begin(l), end(l), ostream_iterator<string>{cout, "n"});
}
7. Компиляция и запуск программы дадут следующий результат. Он не содержит дефисов и показывает, что включает отдельные слова (которые в нашем примере являются отдельными символами):
$ ./split
a
b
c
d
e
f
g
Как это работает
Алгоритм split работает так же, как и std::transform, поскольку принимает начальный и конечный итераторы входного диапазона данных и итератор вывода. Он выполняет какие-то действия с входным диапазоном и