- •4.6. Иерархия итераторов stl: эффективная комбинация алгоритмов и контейнеров
- •4.7. Итераторы вставки
- •4.8. Еще раз о входе и выходе: потоковые итераторы
- •4.9. Спецификация категорий итераторов, требуемых алгоритмами stl
- •4.10. Разработка обобщенных алгоритмов
- •4.11. Почему некоторые алгоритмы требуют более мощные итераторы
- •4.12. Выбор правильного алгоритма
- •4.13. Константные и изменяемые итераторы
- •4.14. Категории итераторов, предоставляемые контейнерами stl
4.9. Спецификация категорий итераторов, требуемых алгоритмами stl
Давайте подробнее рассмотрим, как из спецификаций интерфейсов алгоритмов STL определить, какие итераторы могут использоваться конкретным алгоритмом. В главе 22, "Справочное руководство по обобщенным алгоритмам", приведен следующий интерфейс алгоритма merge:
template <typename Inputlterator1,
typename InputIterator2,
typename Outputlterator>
Outputlterator merge(Inputlterator1 first1,
Inputlterator1 last1,
InputIterator2 first2,
InputIterator2 last2,
Outputlterator result);
В главе 22, "Справочное руководство по обобщенным алгоритмам", используются
следующие соглашения.
• Имена параметров шаблона класса, заканчивающиеся на Iterator или Iterators,
для некоторого целого N означают, что параметры этого типа должны быть итераторами.
• Первая часть имени используется для описания категории, которой должен
принадлежать итератор.
Приведенное выше описание интерфейса гласит, что алгоритм merge требует, чтобы два первых его параметра были входными итераторами одного типа, третий и четвертый параметры также должны быть входными итераторами одинакового типа (который может быть отличен от типа первых двух параметров); а последний параметр алгоритма должен быть выходным итератором.
Только из приведенной спецификации, без знания, что именно делает алгоритм merge и как он реализован, мы можем заключить, что все приведенные выше примеры корректны, включая примеры с использованием объектов istream_iterator и ostream_iterator.
Алгоритм find имеет следующий интерфейс:
template <typename InputIterator, typename T>
Inputlterator find(InputIterator first, Inputlterator last, const T& value);
Таким образом, алгоритм find требует, чтобы его первые два аргумента были входными итераторами, как и возвращаемое алгоритмом значение. Это означает, что find работает почти со всеми итераторами — входными, одно- и двунаправленными, а также произвольного доступа. Исключение составляют только выходные итераторы.
Спецификация еще одного алгоритма, search, указывает, что он более ограничивающий, чем алгоритм find. Алгоритм search ищет подпоследовательность в другой последовательности; он обобщает алгоритмы поиска подстрок наподобие strstr:
template<typename Forwardlterator1, typename ForwardIterator2>
Forwardlterator1 search(Forwardlterator1 first1,
Forwardlterator1 last1,
ForwardIterator2 first2,
ForwardIterator2 last2);
Эта спецификация гласит, что аргументами должны быть однонаправленные итераторы, но почему они не могут быть входными итераторами? Причина в том, что алгоритм search многопроходный: он сохраняет итераторы и использует их для повторного сканирования, поэтому он не будет работать, если попробовать применить его с istream_iterator.
4.10. Разработка обобщенных алгоритмов
Теперь, когда мы понимаем, как спецификация интерфейса говорит нам о том, какие итераторы могут быть использованы, нам не требуется знать реализации алгоритмов. Однако давайте углубим наше понимание категорий итераторов, рассмотрев "начинку" одного алгоритма и изучив некоторые из проектных решений, диктуемых целью достичь максимальной обобщенности при сохранении эффективности. Ниже в очередной раз приведено определение обобщенного алгоритма STL find:
template <typename Inputlterator, typename T>
Inputlterator find(Inputlterator first,
Inputlterator last,
const T& value) {
while (first != last && *first != value)
++first;
return first;
}
Мы видим, что такая реализация будет работать с любым входным итератором.
• Она применяет к итераторам-параметрам только лишь операции ! =, * и ++.
• Она не пытается присваивать значения объектам с применением разыменования *.
• Это однопроходный алгоритм.
Такая реализация find делает алгоритм максимально обобщенным, но это не так при других возможных реализациях. Предположим, что мы сделаем одно маленькое изменение, воспользовавшись < вместо ! = для сравнения итераторов:
while (first < last && *first != value)
++first;
Если мы воспользуемся таким модифицированным алгоритмом find1, то увидим, что алгоритм будет работать для многих контейнеров, таких как обычные массивы, векторы и деки. Но если попытаться использовать его со списками, например
list<int> list1;
// ... Код для вставки значений в in list1
list<int>::iterator
where = findl(list1.begin(),list1.end(),7);
то у нас ничего не получится, потому что новая реализация алгоритма пытается вычислить выражение
list1.begin() < list1.end ()
но оператор < для итераторов list не определен (так как оператор < был бы очень
неэффективным, как вы вскоре убедитесь). Это приводит к ошибке времени компиляции.
Поскольку наличие оператора < гарантируется только для итераторов с произвольным доступом, интерфейс алгоритма find1 должен иметь следующий вид:
template <typename RandomAccessIterator, typename T>
RandomAccessIterator find1(RandomAccessIterator first,
RandomAccessIterator last,
const T& value);
Одним из ключевых достижений при проектировании STL является то, что множество алгоритмов наподобие find может быть закодировано с минимальными требованиями к обращению к данным, что делает их максимально обобщенным. Все реализации наподобие find1, которые предъявляют излишне строгие требования, были тщательно пересмотрены, а их требования снижены до совершенно необходимых, как в случае find, так что в STL остались алгоритмы с минимально возможными допущениями (и тем самым— с максимально возможной обобщенностью).
