C++17 STL Стандартная библиотека шаблонов — страница 29 из 119

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, но вероятность написания кода с ошибками не исчезнет.