Что может быть «наименее плохой реализацией» для итератора над прокси-контейнером? - PullRequest
0 голосов
/ 26 июня 2018

Контекст

Я пытался реализовать массив nD, такой как контейнер. Что-то, что обернет нижележащий контейнер последовательности и позволит обработать его как контейнер контейнеров (из ...): arr[i][j][k] должно быть (в конечном итоге const) ссылкой для _arr[(((i * dim2) + j) * dim3) + k].

Хорошо, пока там, arr[i] просто должен быть классом-оболочкой над подмассивом ...

И когда я попытался реализовать интеграторы, я вдруг понял, что драконы повсюду:

Реальная проблема заключается в том, что, как только у вас есть прокси-контейнер, ни один итератор не может выполнить следующее требование для прямого итератора:

Прямые итераторы [forward.iterators]
...
6 Если a и b являются разыменованными, то a == b тогда и только тогда, когда *a и *b связаны с одним и тем же объектом.

Примеры взяты из самой стандартной библиотеки:

  • vector<bool> известно, что не соблюдаются все требования к контейнерам, поскольку он возвращает прокси вместо ссылок:

    Класс вектора [vector.bool]
    ...
    3 Нет необходимости хранить данные как непрерывное распределение значений bool. Оптимизированное пространство Вместо этого рекомендуется представление битов.
    4 reference - класс, имитирующий поведение ссылок одного бита в векторе.

  • Итератор пути к файловой системе, как известно, является итератором сохранения:

    итераторы пути [fs.path.itr]
    ...
    2 Path :: iterator - это постоянный итератор, удовлетворяющий всем требованиям двунаправленного итератора (27.2.6) за исключением того, что для разыменовываемых итераторов a и b типа path :: iterator с a == b требование не предъявляется *a и *b связаны с одним и тем же объектом.

    и от cppreference :

    Примечания: std :: reverse_iterator не работает с итераторами, которые возвращают ссылку на объект-член (так называемые «скрывающие итераторы»). Примером сохранения итератора является std :: filesystem :: path :: iterator.

Вопрос

В настоящее время я нашел много ссылок о том, почему прокси-контейнеры не являются истинными контейнерами и почему было бы хорошо, если бы прокси-контейнеры и итераторы были разрешены стандартом. Но я до сих пор не понял, что было лучшим из того, что можно было сделать, и каковы были реальные ограничения.

Так что мой вопрос в том, почему прокси-итераторы действительно лучше, чем кеширующие, и какие алгоритмы допустимы для любого из них. Если возможно, я бы очень хотел найти реализацию reference для такого итератора

Для справок, текущая реализация моего кода была представлена ​​на Code Review . Он содержит скрытый итератор (который сразу ломался, когда я пытаюсь использовать std::reverse_iterator)

1 Ответ

0 голосов
/ 26 июня 2018

ОК, у нас есть два похожих, но разных понятия. Итак, давайте выложим их.

Но сначала мне нужно провести различие между именованными требованиями C ++ - pre-20 и реальными языковыми концепциями, созданными для Ranges TS и включенными в C ++ 20. Их обоих называют «понятиями», но они определены по-разному . Таким образом, когда я говорю о концепте с строчными буквами c, я имею в виду требования до C ++ 20. Когда я говорю о Concept-with-a-captial-C, я имею в виду C ++ 20.

Итераторы прокси

Прокси-итераторы - это итераторы, в которых reference - это не value_type&, а какой-то другой тип, который ведет себя как ссылка на value_type. В этом случае *it возвращает значение для этого reference.

Концепция InputIterator не предъявляет никаких требований к reference, за исключением того, что она конвертируется в value_type. Однако концепция ForwardIterator делает явное утверждение, что «reference является ссылкой на T».

Следовательно, прокси-итератор не может соответствовать концепции ForwardIterator. Но может все еще быть InputIterator. Таким образом, вы можете безопасно передавать прокси-итератор любой функции, которая требует только InputIterators.

Итак, проблема с итераторами vector<bool> не в том, что они являются итераторами-посредниками. Они обещают, что они соответствуют концепции RandomAccessIterator (хотя и с использованием соответствующего тега), когда они на самом деле являются только InputIterators и OutputIterators.

Предложение Ranges (в основном), принятое в C ++ 20, вносит изменения в Концепции итераторов, которые позволяют прокси-итераторам использовать для всех итераторов. Таким образом, в диапазонах vector<bool>::iterator действительно соответствует концепции RandomAccessIterator. Поэтому, если у вас есть код, написанный против концепций Ranges, вы можете использовать прокси-итераторы всех видов.

Это очень полезно для таких вещей, как подсчет диапазонов. Вы можете иметь reference и value_type одного типа, так что вы в любом случае имеете дело с целыми числами.

И, конечно, если у вас есть контроль над кодом, потребляющим итератор, вы можете заставить его делать все, что захотите, при условии, что вы не нарушаете концепцию, против которой написан ваш итератор.

Копирование итераторов

Скрывающие итераторы - это итераторы, где reference_type - это (прямо или косвенно) ссылка на объект, сохраненный в итераторе. Поэтому, если вы сделаете копию итератора, копия вернет ссылку на объект, отличный от оригинала, даже если они ссылаются на один и тот же элемент. И когда вы увеличиваете итератор, предыдущие ссылки больше не действительны.

Стерирующие итераторы обычно реализуются, потому что вычисление значения, которое вы хотите вернуть, стоит дорого. Возможно, это будет связано с выделением памяти (например, path::iterator), или, возможно, это будет связано с возможно сложной операцией, которая должна быть выполнена только один раз (например, regex_iterator). Так что вы хотите делать это только тогда, когда это необходимо.

Одна из основ ForwardIterator как концепции (или концепции) заключается в том, что диапазон этих итераторов представляет диапазон значений, которые существуют независимо их итераторов. Это разрешает многопроходную работу, но делает полезными и другие вещи. Вы можете хранить ссылки на элементы в диапазоне, а затем выполнять итерации в другом месте.

Если вам нужен итератор для ForwardIterator или выше, вы должны никогда не делать его итератором. Конечно, стандартная библиотека C ++ не всегда соответствует сама себе. Но обычно он вызывает свои несоответствия.

path::iterator является итератором хранения. Стандарт говорит, что это двунаправленный итератор; однако он также дает этому типу исключение из правила сохранения ссылок / указателей. Это означает, что вы не можете передать path::iterator любому коду, который может полагаться на это правило сохранения.

Так вот, это не значит, что вы ничего не можете передать. Любой алгоритм, который требует только InputIterator, сможет использовать такой итератор, поскольку такой код не может полагаться на это правило. И, конечно, может быть использован любой код, который вы пишете или который конкретно указывает в своей документации, что он не полагается на это правило. Но нет никакой гарантии, что вы можете использовать reverse_iterator для него, даже если он говорит, что это двунаправленный интерфейс.

regex_iterator с еще хуже в этом отношении. Говорят, что они являются ForwardIterators на основе своего тега, но в стандарте никогда не говорится, что они на самом деле являются ForwardIterators (в отличие от path::iterator). И их спецификация как имеющая reference фактическую ссылку на объект-член делает невозможным для них быть настоящими ForwardIterators.

Обратите внимание, что я не делал различий между концепцией до C ++ 20 и концепцией Ranges. Это потому, что концепция ForwardIterator по-прежнему запрещает прятать итераторы. Это по замыслу.

Использование

Теперь, очевидно, вы можете делать все, что захотите, в своем коде. Но код, который вы не контролируете, будет принадлежать его владельцам. Они будут писать против старых концепций, новых концепций или какой-либо другой концепции или требований, которые они определяют. Поэтому ваши итераторы должны быть в состоянии быть совместимыми с их потребностями.

Алгоритмы, добавленные в добавление Ranges, используют новые концепции, поэтому вы всегда можете положиться на них при работе с прокси-итераторами. Однако, насколько я понимаю, концепции диапазона не перенесены в более старые алгоритмы.

Лично я бы посоветовал полностью избежать реализации итераторов. Обеспечивая полную поддержку прокси-итераторов, большинство сохраняющих итераторов можно переписать так, чтобы они возвращали значения , а не ссылки на объекты.

Например, если бы существовал тип path_view, path::iterator мог бы вернуть его вместо полноценного path. Таким образом, если вы хотите сделать дорогую операцию копирования, вы можете. Точно так же regex_iterator s могли бы вернуть копии объекта сопоставления. Новые концепции позволяют работать таким образом, поддерживая прокси-итераторы.

Теперь итераторы-хранилища полезны для кеширования; итераторы могут кэшировать свои результаты, так что повторное использование *it делает дорогую операцию только один раз. Но помните, что проблема с засыпкой итераторов: возвращение ссылки на их содержимое. Вам не нужно , чтобы сделать это только для того, чтобы получить кеширование. Вы можете кэшировать результаты в optional<T> (который вы аннулируете, когда итератор включен / уменьшен). Таким образом, вы все еще можете вернуть значение. Может потребоваться дополнительная копия, но reference не должен быть сложным типом.

Конечно, все это означает, что auto &val = *it; больше не является юридическим кодом. Тем не менее, auto &&val = *it; всегда будет работать. На самом деле это большая часть версии итераторов Range TS.

...