который будут копироваться элементы, copyIf
должен иметь возможность записать разыменованное значение в выходной итератор и увеличить его значение, что дает нам описание оператора вывода. Объявив требования к итераторам с помощью имен параметров шаблона, вы делаете соглашения о вызовах алгоритма понятными без документации. Но зачем использовать две разные категории итераторов?
Имеется, по крайней мере, две причины использования в copyIf
двух различных категорий итераторов. Во-первых, операции с каждым диапазоном несколько отличаются друг от друга, и так как мне никогда не потребуется возвращаться назад по входному диапазону или присваивать ему значения, все, что мне требуется, — это итератор ввода. Аналогично мне никогда не потребуется читать из выходного диапазона, так что все, что здесь требуется, — это итератор вывода. Имеются требования к каждому из итераторов, которые не применимы к другому итератору, так что нет никакого смысла использовать для обоих диапазонов, например, два двунаправленных итератора. Во-вторых, использование различных типов итераторов позволяет вызывающему коду читать из одного типа диапазона и записывать в другой. В примере 7.10 я читаю из vector
и записываю в list
.
vector<string> v(start, end);
list<string> lst;
copyIf(v.begin(), v.end(), back_inserter<list<string> >(lst),
bind2nd(less<string>(), 'cookie'));
Если попробовать сделать то же самое, использовав в алгоритме один и тот же тип итераторов, то он просто не скомпилируется.
В примере 7.10 я в качестве начала выходного диапазона передаю back_inserter
, а не, скажем, итератор, возвращаемый lst.begin
. Это делается потому, что lst не содержит элементов, и в этом алгоритме (как и в стандартном алгоритме копирования) целевой диапазон должен быть достаточно большим, чтобы вместить все элементы, которые будут в него скопированы. В противном случае увеличение итератора вывода в copyIf
приведет к неопределенному поведению. back_inserter
возвращает итератор вывода, который при его увеличении вызывает для контейнера метод push_back
. В результате этого при каждом увеличении выходного итератора размер lst
увеличивается на один. Более подробно шаблон класса back_inserter
я описываю в рецепте 7.5.
При написании собственного алгоритма для работы с диапазонами (т.е. со стандартными контейнерами) вы должны работать с аргументами-итераторами, а не с аргументами-контейнерами. У вас может возникнуть желание объявить copyIf
так, чтобы он принимал два контейнера, а не итератор исходного и результирующего диапазонов, но это менее обобщенное решение, чем диапазоны. Во-первых, если передавать аргументы-контейнеры, то станет невозможно работать с подмножеством элементов контейнера. Далее, в теле copyIf
появится зависимость от методов контейнеров begin
и end
, которые дадут требуемый диапазон, и возвращаемый тип будет зависеть от типа контейнера, используемого в качестве выходного. Это означает, что использование в copyIf
нестандартных диапазонов, таких как встроенные массивы или собственные контейнеры, работать не будет. Именно по этим и некоторым другим причинам все стандартные алгоритмы оперируют с диапазонами.
Наконец, если вы пишете свой алгоритм, дважды убедитесь, что стандартные алгоритмы вас не устраивают. На первый взгляд они могут казаться очень простыми алгоритмами, но их кажущаяся простота проистекает из их обобщенности, и в девяти случаях из десяти их можно расширить так, что они подойдут для новых задач. Иногда следует стремиться к повторному использованию стандартных алгоритмов, так как это дает гарантию переносимости и эффективности.
Рецепт 7.5.
7.11. Печать диапазона в поток
Имеется диапазон элементов, который требуется напечатать в поток, например, в cout
с целью отладки.
Напишите шаблон функции, который принимает диапазон или контейнер, перебирает все его элементы и использует алгоритм сору
и ostream_iterator
для записи. Если требуется дополнительное форматирование, напишите свой простой алгоритм, который перебирает диапазон и печатает каждый элемент в поток. (См. пример 7.11)
#include <iostream>
#include <string>
#include <algorithm>
#include <iterator>
#include <vector>
using namespace std;
int main() {
// Итератор ввода - это противоположность итератору вывода: он
// читает элементы из потока так. как будто это контейнер.
cout << 'Введите несколько строк: ';
istream_iterator<string> start(cin);
istream_iterator<string> end;
vector<string> v(start, end);
// Используем выходной поток как контейнер, используя
// output_iterator. Он создает итератор вывода, для которого запись
// в каждый элемент эквивалентна записи в поток.
copy(v.begin(), v.end(), ostreamIterator<string>(cout, ', '));
}
Вывод примера 7.11 может выглядеть так.
Введите несколько строк: z x y a b с
^Z
z, x, y, a, b, с,
Потоковый итератор — это итератор, который основан на потоке, а не на диапазоне элементов контейнера, и позволяет рассматривать поток как итератор ввода (читать из разыменованного значения и увеличивать итератор) или итератор вывода (аналогично итератору ввода, но для записи в разыменованное значение вместо чтения из него). Это облегчает чтение значений (особенно строк) из потока, что делается в нескольких других примерах этой главы, и запись значений в поток, что делается в примере 7.11. Я знаю, что этот рецепт посвящен записи диапазона в поток, но позвольте мне немного отойти от этой задачи и, поскольку я использую потоковые итераторы во многих примерах этой главы, объяснить, что это такое.
В примере 7.11 показаны три ключевые части istream_iterator
. Первая часть — это создание istream_iterator
, указывающего на начало потокового ввода. Это делается вот так.
istream_iterator<string> start(cin);
В результате создается итератор с именем start
, который указывает на первый