Шрифт:
Интервал:
Закладка:
Обратный итератор содержит обычный итератор и подражает его интерфейсу, но изменяет операцию инкремента на операцию декремента.
Еще одна деталь связана с позициями начала и конца. Взглянем на рис. 3.4, на котором показана стандартная последовательность чисел, хранящаяся в итерабельном диапазоне данных. Если она содержит числа от 1 до 5, то начальный итератор должен указывать на элемент 1, а конечный итератор — на элемент, стоящий сразу после 5.
При определении обратных итераторов итератор rbegin должен указывать на элемент 5, а итератор rend — на элемент, стоящий сразу перед 1. Переверните книгу вверх ногами — и убедитесь в том, что это имеет смысл.
Если мы хотим, чтобы наши собственные классы-контейнеры поддерживали обратный перебор, то не нужно реализовывать все эти детали самостоятельно, можно просто обернуть обычные итераторы в обратные с помощью вспомогательной функции std::make_reverse_iterator, и она сделает всю работу за нас.
Завершение перебора диапазонов данных с использованием ограничителей
Как алгоритмы STL, так и основанные на диапазонах циклы for предполагают, что начальная и конечная позиции для перебора известны заранее. В некоторых ситуациях, однако, нельзя узнать конечную позицию до того, как она будет достигнута при переборе.
Самый простой пример такой ситуации — это перебор в стиле С простых строк, длина которых во время выполнения неизвестна. Код, итерирующий по таким строкам, обычно выглядит следующим образом:
for (const char *c_ponter = some_c_string; *c_pointer != ' '; ++c_pointer)
{
const char c = *c_pointer;
// сделаем что-нибудь с переменной c
}
Единственный способ поработать с этими строками в основанном на диапазоне цикле for заключается в том, чтобы обернуть их в объект std::string, который поддерживает функции begin() и end():
for (char c : std::string(some_c_string)) { /* сделаем что-нибудь с c */ }
Однако конструктор класса std::string будет итерировать по всей строке до того, как этим сможет заняться созданный нами цикл. В С++17 появился класс std::string_view, но его конструктор также один раз проитерирует по всей строке. Короткие строки не стоят таких хлопот, но это только пример одного из проблемных классов, для которого подобная возня может быть оправдана в других ситуациях. Итератор std::istream_iterator тоже сталкивается с подобными случаями в момент приема входящих данных из std::cin, поскольку его конечный итератор не может реалистично указывать на конец потока данных, когда пользователь еще вводит текст.
Начиная с C++17 начальный и конечный итераторы не обязаны иметь один тип. В данном разделе мы продемонстрируем, как правильно использовать это небольшое изменение в правилах.
Как это делается
В этом примере мы создадим итератор и класс диапазона, который позволит проитерировать по строке неизвестной длины, не зная конечной позиции заранее.
1. Сначала, как и всегда, включим заголовочные файлы:
#include <iostream>
2. Ограничитель итератора — самый важный элемент этого раздела. Удивительно, но определение его класса остается полностью пустым:
class cstring_iterator_sentinel {};
3. Теперь реализуем итератор. Он будет содержать указатель на строку, которая и станет тем контейнером, по которому мы будем итерировать:
class cstring_iterator {
const char *s {nullptr};
4. В конструкторе просто инициализируется внутренний указатель на строку, предоставляемую пользователем. Сделаем конструктор явным, чтобы предотвратить неявные преобразования строк к строковым итераторам:
public:
explicit cstring_iterator(const char *str)
: s{str}
{}
5. При разыменовании итератор в какой-то момент просто вернет символьное значение в этой позиции:
char operator*() const { return *s; }
6. Операция инкремента для итератора просто инкрементирует позицию в строке:
cstring_iterator& operator++() {
++s;
return *this;
}
7. Здесь начинается самое интересное. Мы реализуем оператор сравнения !=, который используется алгоритмами STL и основанным на диапазоне циклом for. Однако в этот раз мы будем реализовывать его для сравнения итераторов не с другими итераторами, а с ограничителями. При сравнении итераторов можно проверить только тот факт, что их внутренние указатели на строку указывают на один и тот же адрес; это несколько ограничивает наши возможности. Сравнивая итератор с пустым объектом-ограничителем, можно применить совершенно другую семантику: проверить, указывает ли наш итератор на завершающий символ ' ', поскольку он представляет собой конец строки!
bool operator!=(const cstring_iterator_sentinel) const {
return s != nullptr && *s != ' ';
}
};
8. Чтобы использовать эту возможность в основанном на диапазоне цикле for, нужен класс диапазона, который предоставит конечный и начальный итераторы:
class cstring_range {
const char *s {nullptr};
9. Единственное, что пользователь должен предоставить при создании экземпляра этого класса, — строка, по которой мы будем итерировать:
public:
cstring_range(const char *str)
: s{str}
{}
10. Вернем обычный итератор cstring_iterator из функции begin(), который указывает на начало строки. Из функции end() мы вернем тип ограничителя. Обратите внимание: без типа ограничителя мы также будем возвращать итератор, но как же узнать о достижении конца строки, если мы не нашли его заранее?
cstring_iterator begin() const {
return cstring_iterator{s};
}
cstring_iterator_sentinel end() const {
return {};
}
};
11. На этом все. Мы можем мгновенно применить итератор. Строки, которые поступают от пользователя, представляют собой лишь один пример входных данных, чью длину мы не знаем заранее. Чтобы заставить пользователя предоставить какие-нибудь входные данные, мы станем завершать работу программы, если тот не указал хотя бы один параметр при ее запуске в оболочке:
int main(int argc, char *argv[])
{
if (argc < 2) {
std::cout << "Please provide one parameter.n";
return 1;
}
12. Если программа все еще работает, то мы знаем, что в argv[1] содержится какая-то пользовательская строка:
for (char c : cstring_range(argv[1])) {
std::cout << c;
}
std::cout << 'n';
}