|
||||
|
Итераторы На первый взгляд итераторы представляются предметом весьма простым. Но стоит присмотреться повнимательнее, и вы заметите, что стандартные контейнеры STL поддерживают четыре разных типа итераторов: iterator, const_iterator, reverse_iteratorи const_reverse_iterator. Проходит совсем немного времени, и выясняется, что в некоторых формах insert и erase только один из этих четырех типов принимается контейнером. И здесь начинаются вопросы. Зачем нужны четыре типа итераторов? Существует ли между ними какая-либо связь? Можно ли преобразовать итератор от одного типа к другому? Можно ли смешивать разные типы итераторов при вызове алгоритмов и вспомогательных функций STL? Как эти типы связаны с контейнерами и их функциями? В настоящей главе вы найдете ответы на эти вопросы, а также поближе познакомитесь с разновидностью итераторов, которой обычно не уделяют должного внимания: istreambuf_iterator. Если вам нравится STL, но не устраивает быстродействие istream_iteratorпри чтении символьных потоков, возможно, istreambuf_iteratorпоможет справиться с затруднениями. Совет 26. Старайтесь использовать iterator вместо const_iterator, reverse_iterator и const_reverse_iterator Как известно, каждый стандартный контейнер поддерживает четыре типа итераторов. Для контейнера container<T>тип iteratorработает как T*тогда как const_iteratorработает как const T*(также встречается запись T const*). При увеличении iteratorили const_iteratorпроисходит переход к следующему элементу контейнера в прямом порядке перебора (от начала к концу контейнера). Итераторы reverse_iteratorи const_reverse_iteratorтакже работают как T*и const T*соответственно, но при увеличении эти итераторы переходят к следующему элементу в обратном порядке перебора (от конца к началу). Рассмотрим несколько сигнатур insertи eraseв контейнере vector<T>: iterator insert(iterator position, const T& x); iterator erase(iterator position); iterator erase(iterator rangeBegin, iterator rangeEnd); Аналогичные функции имеются у всех стандартных контейнеров, но тип возвращаемого значения определяется типом контейнера. Обратите внимание: перечисленные функции требуют передачу параметров типа iterator. Не const_iterator, не reverse_iteratorи не const_reverse_iterator— только iterator. Хотя контейнеры поддерживают четыре типа итераторов, один из этих типов обладает привилегиями, отсутствующими у других типов. Тип iteratorзанимает особое место. На следующей диаграмме показаны преобразования, возможные между итераторами разных типов. Из рисунка следует, что iteratorпреобразуется в const_iteratorи reverse_iterator, а reverse_iterator— в const_reverse_iterator. Кроме того, reverse_iteratorпреобразуется в iteratorпри помощи функции baseтипа reverse_iterator, а const_reverse_iteratorаналогичным образом преобразуется в const_iterator. Однако из рисунка не видно, что итераторы, полученные при вызове base, могут оказаться не теми, которые вам нужны. За подробностями обращайтесь к совету 28. Обратите внимание: не существует пути от const_iteratorк iteratorили от const_reverse_iteratorк reverse_iterator. Из этого важного обстоятельства следует, что const_iteratorи const_reverse_iteratorмогут вызвать затруднения с некоторыми функциями контейнеров. Таким функциям необходим тип iterator, а из-за отсутствия обратного перехода от const-итераторов к iteratorпервые становятся в целом бесполезными, если вы хотите использовать их для определения позиции вставки или удаления элементов. Однако не стоит поспешно заключать, что const-итераторы вообще бесполезны. Это не так. Они прекрасно работают с алгоритмами, поскольку для алгоритмов обычно подходят все типы итераторов, относящиеся к нужной категории. Кроме того, const-итераторы подходят для многих функций контейнеров. Проблемы возникают лишь с некоторыми формами insertи erase. Обратите внимание на формулировку: const-итераторы становятся в целом бесполезными, если вы хотите использовать их для определения позиции вставки или удаления элементов. Называть их полностью бесполезными было бы неправильно. Const-итераторы могут принести пользу, если вы найдете способ получения iteratorдля const_iteratorили const_reverse_iterator. Такое возможно часто, но далеко не всегда, причем даже в благоприятном случае решение не очевидно, да и эффективным его не назовешь. В двух словах этот вопрос не изложить, если вас заинтересуют подробности — обращайтесь к совету 27. А пока имеющаяся информация позволяет понять, почему типу iteratorотдается предпочтение перед его const- и reverse-аналогами. • Некоторым версиям insertи eraseпри вызове должен передаваться тип iterator. Const- и reverse-итераторы им не подходят. • Автоматическое преобразование const-итератора в iteratorневозможно, а методика получения iteratorна основании const_iterator(совет 27) применима не всегда, да и эффективность ее не гарантируется. • Преобразование reverse_iteratorв iteratorможет требовать дополнительной регулировки итератора. В совете 28 рассказано, когда и почему возникает такая необходимость. Из сказанного следует однозначный вывод: если вы хотите работать с контейнерами просто и эффективно и по возможности застраховаться от нетривиальных ошибок, выбирайте iteratorвместо его const- и reverse-аналогов. На практике выбирать обычно приходится между iteratorи const_iterator. Выбор между iteratorи reverse_iteratorчасто происходит помимо вашей воли — все зависит от того, в каком порядке должны перебираться элементы контейнера (в прямом или в обратном). А если после выбора reverse_iteratorпотребуется вызвать функцию контейнера, требующую iterator, вызовите функцию base(возможно, с предварительной регулировкой смещения — см. совет 28). При выборе между iteratorи const_iteratorрекомендуется выбирать iteratorдаже в том случае, если можно обойтись const_iterator, а использование iteratorне обусловлено необходимостью вызова функции контейнера. В частности, немало хлопот возникает при сравнениях iteratorс const_iterator. Думаю, вы согласитесь, что следующий фрагмент выглядит вполне логично: typedef deque<int> IntDeque; // Определения типов typedef IntDeque:iterator Iter; // упрощают работу typedef IntDeque::const_iterator ConstIter; // с контейнерами STL // и типами итераторов iter i; ConstIter ci; … // i и ci указывают на элементы // одного контейнера if (i==ci)… // Сравнить iterator //c const_iterator В данном примере происходит обычное сравнение двух итераторов контейнера, подобные сравнения совершаются в STL сплошь и рядом. Просто один объект относится к типу iterator, а другой — к типу const_iterator. Проблем быть не должно — iteratorавтоматически преобразуется в const_iterator, и в сравнении участвуют два const_iterator. Именно это и происходит в хорошо спроектированных реализациях STL, но в некоторых случаях приведенный фрагмент не компилируется. Причина заключается в том, что такие реализации объявляют operator==функцией класса const_iteratorвместо внешней функции. Впрочем, вас, вероятно, больше интересуют не корни проблемы, а ее решение, которое заключается в простом изменении порядка итераторов: if (c==i)… // Обходное решение для тех случаев, // когда приведенное выше сравнение не работает Подобные проблемы возникают не только при сравнении, но и вообще при смешанном использовании iteratorи const_iterator(или reverse_iteratorи const_reverse_iterator) в одном выражении, например, при попытке вычесть один итератор произвольного доступа из другого: if (i-ci>=3)… // Если i находится минимум в трех позициях после ci… ваш (правильный) код будет несправедливо отвергнут компилятором, если итераторы относятся к разным типам. Обходное решение остается прежним (перестановка iи ci), но в этом случае приходится учитывать, что i-ciне заменяется на ci-i: if (c+3<=i)… // Обходное решение на случай, если // предыдущая команда не компилируется Простейшая страховка от подобных проблем заключается в том, чтобы свести к минимуму использование разнотипных итераторов, а это в свою очередь подсказывает, что вместо const_iteratorследует использовать iterator. На первый взгляд отказ от const_iteratorтолько для предотвращения потенциальных недостатков реализации (к тому же имеющих обходное решение) выглядит неоправданным, но с учетом особого статуса iteratorв некоторых функциях контейнеров мы неизбежно приходим к выводу, что итераторы const_iteratorменее практичны, а хлопоты с ними иногда просто не оправдывают затраченных усилий. Совет 27. Используйте distance и advance для преобразования const_iterator в iterator Как было сказано в совете 26, некоторые функции контейнеров, вызываемые с параметрами-итераторами, ограничиваются типом iterator; const_iteratorим не подходит. Что же делать, если имеется const_iteratorи вы хотите вставить новый элемент в позицию контейнера, обозначенную этим итератором? Const_iteratorнеобходимо каким-то образом преобразовать в iterator, и вы должныпринять в этом активное участие, поскольку, как было показано в совете 26, автоматического преобразования const_iteratorв iterator не существует. Я знаю, о чем вы думаете. «Если ничего не помогает, берем кувалду», не так ли? В мире C++ это может означать лишь одно: преобразование типа. Стыдитесь. И где вы набрались таких мыслей? Давайте разберемся с вредным заблуждением относительно преобразования типа. Посмотрим, что происходит при преобразовании const_iteratorв iterator: typedef deque<int> IntDeque; // Вспомогательные определения типов typedef IntDeque::iterator Iter; typedef IntDeque::const_iterator ConstIter; ConstIter ci; // ci - const iterator Iter i(ci); // Ошибка! He существует автоматического // преобразования const_iterator // в iterator Iter i(const_cast<Iter>(ci)); // Ошибка! Преобразование const_iterator // в iterator невозможно! В приведенном примере используется контейнер deque, но аналогичный результат будет получен и для list, set, muliset, mulimapи хэшированных контейнеров, упоминавшихся в совете 25. Возможно, строка с преобразованием будет откомпилирована дляvector и string, но это особые случаи, которые будут рассмотрены ниже. Почему же для этих типов контейнеров преобразование не компилируется? Потому что iteratorи const_iteratorотносятся к разным классам, и сходства между ними не больше, чем между stringи complex<double>. Попытка преобразования одного типа в другой абсолютно бессмысленна, поэтому вызов const_castбудет отвергнут. Попытки использования static_cast, reintepreter_castи преобразования в стиле C приведут к тому же результату. Впрочем, некомпилируемое преобразование все же может откомпилироваться, если итераторы относятся к контейнеру vectorили string. Это объясняется тем, что в реализациях данных контейнеров в качестве итераторов обычно используются указатели. В этих реализациях vector<T>::iteratorявляется определением типа для T*, vector<T>::const_iterator— для const T*, string::iterator— для char*, а string::const_iterator— для const char*. В реализациях данных контейнеров преобразование const_iteratorв iteratorвызовом const_castкомпилируется и даже правильно работает, поскольку оно преобразует const T*в T*. Впрочем, даже в этих реализациях reverse_iteratorи const_reverse_iteratorявляются полноценными классами, поэтому const_castне позволяет преобразовать const_reverse_iteratorв reverse_iterator. Кроме того, как объясняется в совете 50, даже реализации, в которых итераторы контейнеров vectorи stringпредставлены указателями, могут использовать это представление лишь при компиляции окончательной (release) версии. Все перечисленные факторы приводят к мысли, что преобразование const-итераторов в итераторы не рекомендуется и для контейнеров vectorи string, поскольку переносимость такого решения будет сомнительной. Если у вас имеется доступ к контейнеру, от которого был взят const_iterator, существует безопасный, переносимый способ получения соответствующего типа iteratorбез нарушения системы типов. Ниже приведена основная часть этого решения (возможно, перед компиляцией потребуется внести небольшие изменения): typedef deque<int> IntDeque; //См. ранее typedef IntDeque::iterator Iter; typedef IntDeque::const_iterator ConstIter; IntDeque d; ConstIter ci; … // Присвоить ci ссылку на d Iter i(d.begin()); // Инициализировать i значением d.begin() advance(i, distance(i, ci)); // Переместить i в позицию ci Решение выглядит настолько простым и прямолинейным, что это невольно вызывает подозрения. Чтобы получить iterator, указывающий на тот же элемент контейнера, что и const_iterator, мы создаем новый iteratorв начале контейнера и перемещаем его вперед до тех пор, пока он не удалится на то же расстояние, что и const_iterator! Задачу упрощают шаблоны функций advanceи distance, объявленные в <iterator>. Distanceвозвращает расстояние между двумя итераторами в одном контейнере, a advanceперемещает итератор на заданное расстояние. Когда итераторы iи ciотносятся к одному контейнеру, выражение advance(i, distance(i, ci))переводит их в одну позицию контейнера. Все хорошо, если бы этот вариант компилировался… но этого не происходит. Чтобы понять причины, рассмотрим объявление distance: template<typename InputIterator> typename iterator_traits<InputIterator>::difference_type distance(InputIterator first, InputIterator last); Не обращайте внимания на то, что тип возвращаемого значения состоит из 56 символов и содержит упоминания зависимых типов (таких как differenceype). Вместо этого проанализируем использование параметра-типа InputIterator: template<typename InputIterator> typename iterator_traits<InputIterator>::difference_type distance(InputIterator first, InputIterator last); При вызове distanceкомпилятор должен определить тип, представленный InputIterator, для чего он анализирует аргументы, переданные при вызове. Еще раз посмотрим на вызов distanceв приведенном выше коде: advance(i, distance(i,ci)); // Переместить i в позицию ci При вызове передаются два параметра, iи ci. Параметр iотносится к типу iter, который представляет собой определение типа для deque<int>::iterator. Для компилятора это означает, что InputIteratorпри вызове distanceсоответствует типу deque<int>::iterator. Однако ciотносится к типу ConstIter, который представляет собой определение типа для deque<int>::const_iterator. Из этого следует, что InputIteratorсоответствует типу deque<int>::const_iterator. InputIteratorникак не может соответствовать двум типам одновременно, поэтому вызов distanceзавершается неудачей и каким-нибудь запутанным сообщением об ошибке, из которого можно (или нельзя) понять, что компилятор не смог определить тип InputIterator. Чтобы вызов нормально компилировался, необходимо ликвидировать неоднозначность. Для этого проще всего явно задать параметр-тип, используемый distance, и избавить компилятор от необходимости определять его самостоятельно: advanced.distance<ConstIter>(i, ci)); // Вычислить расстояние между // i и ci (как двумя const_iterator) // и переместить i на это расстояние Итак, теперь вы знаете, как при помощи advanceи distanceполучить iterator, соответствующий заданному const_iterator, но до настоящего момента совершенно не рассматривался вопрос, представляющий большой практический интерес: насколько эффективна данная методика? Ответ прост: она эффективна настолько, насколько это позволяют итераторы. Для итераторов произвольного доступа, поддерживаемых контейнерами vector, string, dequeи т. д., эта операция выполняется с постоянным временем. Для двусторонних итераторов (к этой категории относятся итераторы других стандартных контейнеров, а также некоторых реализаций хэшированных контейнеров — см. совет 25) эта операция выполняется с линейным временем. Поскольку получение iterator, эквивалентного const_iterator, может потребовать линейного времени, и поскольку это вообще невозможно сделать при недоступности контейнера, к которому относится const_iterator, проанализируйте архитектурные решения, вследствие которых возникла необходимость получения iteratorпо const_iterator. Результат такого анализа станет дополнительным доводом в пользу совета 26, рекомендующего отдавать предпочтение iteratorперед const- и reverse-итераторами. Совет 28. Научитесь использовать функцию base При вызове функции baseдля итератора reverse_iteratorбудет получен «соответствующий» iterator, однако из сказанного совершенно не ясно, что же при этом происходит. В качестве примера рассмотрим следующий фрагмент, который заносит в вектор числа 1–5, устанавливает reverse_iteratorна элемент 3 и инициализирует iteratorфункцией base: vector<int> v; v.reserve(5); //См. совет 14 for (int i=1; i<=5; ++i){ // Занести в вектор числа 1-5 v.push_back(i); } vector<int>::reverse_iterator ri = // Установить ri на элемент 3 find(v.rbegin(), v.rend(), 3); vector<int>::iterator i(ri.base()); // Присвоить i результат вызова base // для итератора ri После выполнения этого фрагмента ситуация выглядит примерно так: На рисунке видно характерное смещение reverse_iteratorи соответствующего базового итератора, воспроизводящего смещение begin()и end()по отношению к begin()и end(), но найти на нем ответы на некоторые вопросы не удается. В частности, рисунок не объясняет, как использовать iдля выполнения операций, которые должны были выполняться с ri. Как упоминалось в совете 26, некоторые функции контейнеров принимают в качестве параметров-итераторов только iterator. Поэтому если вы, допустим, захотите вставить новый элемент в позицию, определяемую итератором ri, сделать это напрямую не удастся; функция insertконтейнера vectorне принимает reverse_iterator. Аналогичная проблема возникает при удалении элемента, определяемого итератором ri. Функции erase не соглашаются на reverse_iteratorи принимают только iterator. Чтобы выполнить удаление или вставку, необходимо преобразовать reverse_iteratorв iteratorпри помощи base, а затем воспользоваться iterator для выполнения нужной операции. Допустим, потребовалось вставить в vновый элемент в позиции, определяемой итератором ri. Для определенности будем считать, что вставляется число 99. Учитывая, что riна предыдущем рисунке используется для перебора справа налево, а новый элемент вставляется перед позицией итератора, определяющего позицию вставки, можно ожидать, что число 99 окажется перед числом 3 в обратном порядке перебора. Таким образом, после вставки вектор vбудет выглядеть так: Конечно, мы не можем использовать riдля обозначения позиции вставки, поскольку это не iterator. Вместо этого необходимо использовать i. Как упоминалось выше, когда riуказывает на элемент 3, i(то есть r. base()) указывает на элемент 4. Именно на эту позицию должен указывать итератор i, чтобы вставленный элемент оказался в той позиции, в которой он бы находился, если бы для вставки можно было использовать итератор ri. Заключение: • чтобы эмулировать вставку в позицию, заданную итератором riтипа reverse_iterator, выполните вставку в позицию r.base(). По отношению к операции вставки riи r.base()эквивалентны, но r.base()в действительности представляет собой iterator, соответствующий ri. Рассмотрим операцию удаления элемента. Вернемся к взаимосвязи между riи исходным вектором (по состоянию на момент, предшествующий вставке значения 99): Для удаления элемента, на который указывает итератор ri, нельзя просто использовать i, поскольку этот итератор ссылается на другой элемент. Вместо этого нужно удалить элемент, предшествующий i. Заключение: • чтобы эмулировать удаление в позиции, заданной итератором riтипа reverse_iterator, выполните удаление в позиции, предшествующей ri.base(). По отношению к операции удаления riи ri.base()не эквивалентны, a ri.base()не является объектом iterator, соответствующим ri. Однако к коду стоит присмотреться повнимательнее, поскольку вас ждет сюрприз: vector<int> v; … // См. ранее. В вектор v заносятся // числа 1-5 vector<int>::reverse_iterator ri = // Установить ri на элемент 3 find(v.rbegin(), v.rend(), 3); v.erase(--ri.base()); // Попытка стирания в позиции. // предшествующей ri-base(): // для вектора обычно // не компилируется Решение выглядит вполне нормально. Выражение --ri.base()правильно определяет элемент, предшествующий удаляемому. Более того, приведенный фрагмент будет нормально работать для всех стандартных контейнеров, за исключением vectorи string. Наверное, он бы мог работать и для этих контейнеров, но во многих реализациях vectorи stringон не будет компилироваться. В таких реализациях типы iterator(и const_iterator) реализованы в виде встроенных указателей, поэтому результатом вызова i.base()является указатель. В соответствии с требованиями как C, так и C++ указатели, возвращаемые функциями, не могут модифицироваться, поэтому на таких платформах STL выражения типа --i.base()не компилируются. Чтобы удалить элемент в позиции, заданной итератором reverse_iterator, и при этом сохранить переносимость, необходимо избегать модификации возвращаемого значения base. Впрочем, это несложно. Если мы не можем уменьшить результат вызова base, значит, нужно увеличить reverse_iteratorи после этого вызвать base! … //См. ранее v.erase((++ri).base()); // Удалить элемент, на который указывает ri; // команда всегда компилируется Такая методика работает во всех стандартных контейнерах и потому считается предпочтительным способом удаления элементов, определяемых итератором reverse_iterator. Вероятно, вы уже поняли: говорить о том, что функция baseкласса reverse_iteratorвозвращает «соответствующий» iterator, не совсем правильно. В отношении вставки это действительно так, а в отношении удаления — нет. При преобразовании reverse_iteratorв iteratorважно знать, какие операции будут выполняться с полученным объектом iterator. Только в этом случае вы сможете определить, подойдет ли он для ваших целей. Совет 29. Рассмотрите возможность использования istreambuf_iterator при посимвольном вводе Предположим, вы хотите скопировать текстовый файл в объект string. На первый взгляд следующее решение выглядит вполне разумно: ifstream inputFile("interestringData.txt"); string fileData(istream_iterator<char>(inputFile)), // Прочитать inputFile istream iterator<char>()); // в fileData Но вскоре выясняется, что приведенный синтаксис не копирует в строку пропуски ( whitespace), входящие в файл. Это объясняется тем, что istream_iteratorпроизводит непосредственное чтение функциями operator<<, а эти функции по умолчанию не читают пропуски. Чтобы сохранить пропуски, входящие в файл, достаточно включить режим чтения пропусков сбросом флага skipwsдля входного потока: ifstream inputFile("interestingData.txt"); inputFile.unset(ios::skipws); // Включить режим // чтения пропусков // в inputFile string fileData(istream_iterator<char>(inputFile)), // Прочитать inputFile istream_iterator<char>()); // в fileData. Теперь все символы InputFileкопируются в fileData. Кроме того, может выясниться, что копирование происходит не так быстро, как вам хотелось бы. Функции operator<<, от которых зависит работа stream_iterator, производят форматный ввод, а это означает, что каждый вызов сопровождается многочисленными служебными операциями. Они должны создать и уничтожить объекты sentry(специальные объекты потоков ввода-вывода, выполняющие начальные и завершающие операции при каждом вызове operator<<); они должны проверить состояние флагов, влияющих на их работу (таких, как skpws); они должны выполнить доскональную проверку ошибок чтения, а в случае обнаружения каких-либо проблем — проанализировать маску исключений потока и определить, нужно ли инициировать исключение. Все перечисленные операции действительно важны при форматном вводе, но если ваши потребности ограничиваются чтением следующего символа из входного потока, без них можно обойтись. Более эффективное решение основано на использовании неприметного итератора istreambuf_iterator. Итераторы istreambuf_iteratorработают аналогично istream_iterator, но если объекты istream_iterator<char>читают отдельные символы из входного потока оператором <<, то объекты streambuf_iteratorобращаются прямо к буферу потока и непосредственно читают следующий символ (выражаясь точнее, объект streambuf_iterator<char>читает следующий символ из входного потока sвызовом s.rdbuf ()->sgetc()). Перейти на использование istreambuf_iteratorпри чтении файла так просто, что даже программист Visual Basic сделает это со второй попытки: ifstream inputFile("interestingData.txt"); string fileData(istreambuf_iterator<char>(inputFile)), istreambuf_iterator<char>0); На этот раз сбрасывать флаг skpws не нужно, итераторы streambuf_iteratorникогда не пропускают символы при вводе и просто возвращают следующий символ из буфера. По сравнению с istream_iteratorэто происходит относительно быстро. В проведенных мною простейших тестах выигрыш по скорости достигал 40%, хотя в вашем случае цифры могут быть другими. Не удивляйтесь, если быстродействие будет расти со временем; итераторы istreambuf_iteratorнаселяют один из заброшенных уголков STL, и авторы реализаций еще недостаточно позаботились об их оптимизации. Например, в моих примитивных тестах итераторы istreambuf_iteratorодной из реализаций работали всего на 5% быстрее, чем istream_iterator. В таких реализациях остается широкий простор для оптимизации istreambuf_iterator. Если вы планируете читать из потока по одному символу, не нуждаетесь в средствах форматирования ввода и следите за эффективностью выполняемых операций, три лишних символа на итератор — не такая уж дорогая цена за заметный рост быстродействия. При неформатном посимвольном вводе всегда рассматривайте возможность применения sreambuf_iterator. Раз уж речь зашла о буферизованных итераторах, следует упомянуть и об использовании ostreambuf_iteratorпри неформатном посимвольном выводе. По сравнению с ostream_iteratorитераторы ostreambuf_iteratorобладают меньшими затратами (при меньших возможностях), поэтому обычно они превосходят их по эффективности. |
|
||
Главная | Контакты | Нашёл ошибку | Прислать материал | Добавить в избранное |
||||
|