1
до 5
, то начальный итератор должен указывать на элемент 1
, а конечный итератор — на элемент, стоящий сразу после 5
.При определении обратных итераторов итератор
rbegin
должен указывать на элемент 5
, а итератор rend
— на элемент, стоящий сразу перед1
. Переверните книгу вверх ногами — и убедитесь в том, что это имеет смысл.Если мы хотим, чтобы наши собственные классы-контейнеры поддерживали обратный перебор, то не нужно реализовывать все эти детали самостоятельно, можно просто обернуть обычные итераторы в обратные с помощью вспомогательной функции
std::make_reverse_iterator
, и она сделает всю работу за нас. Завершение перебора диапазонов данных с использованием ограничителей
Как алгоритмы STL, так и основанные на диапазонах циклы
for
предполагают, что начальная и конечная позиции для перебора известны заранее. В некоторых ситуациях, однако, нельзя узнать конечную позицию до того, как она будет достигнута при переборе.Самый простой пример такой ситуации — это перебор в стиле С простых строк, длина которых во время выполнения неизвестна. Код, итерирующий по таким строкам, обычно выглядит следующим образом:
for (const char *c_ponter = some_c_string;
*c_pointer != '\0'; ++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
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
. Однако в этот раз мы будем реализовывать его для сравнения итераторов не с другими итераторами, а с ограничителями. При сравнении итераторов можно проверить только тот факт, что их внутренние указатели на строку указывают на один и тот же адрес; это несколько ограничивает наши возможности. Сравнивая итератор с пустым объектом-ограничителем, можно применить совершенно другую семантику: проверить, указывает ли наш итератор на завершающий символ '\0'
, поскольку он представляет собой конец строки!
bool operator!=(const cstring_iterator_sentinel) const {
return s != nullptr && *s != '\0';
}
};
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';
}
13. Компиляция и запуск программы дадут следующий результат:
$ ./main "abcdef"
abcdef
Цикл выводит на экран введенные нами данные, и это неудивительно, поскольку мы рассмотрели небольшой пример реализации диапазона итераторов на базе ограничителей. Такой способ завершения перебора позволит вам реализовать собственные итераторы, если вы столкнетесь с ситуацией, когда сравнение с конечной позицией не помогает.
Автоматическая проверка кода итераторов с помощью проверяемых итераторов
Хотя итераторы очень полезны и предоставляют общие интерфейсы, есть шанс использовать их неправильно, как и указатели. При работе с указателями код нужно писать так, чтобы никогда не разыменовывать их в те моменты, когда они указывают на некорректные точки в памяти. То же верно и для итераторов, но для них предусмотрено множество правил, которые позволяют определить, корректен ли итератор. С этими правилами можно ознакомиться, изучив документацию к STL, но вероятность написания кода с ошибками не исчезнет.