Что может быть «наименее плохой реализацией» для итератора над прокси-контейнером?

контекст

Я пытался реализовать массив nD, как контейнер. Что-то, что обернет базовый контейнер последовательности и позволит обработать его как контейнер контейнеров (of …): arr[i][j][k] должен быть (в конце концов, постоянным) ссылкой на _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 :: итератор с a == bнет требований
    тот*a а также *b связаны с одним и тем же объектом.

    и из cppreference:

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

Вопрос

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

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

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

1

Решение

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

Но сначала мне нужно провести различие между именованными требованиями C ++ — pre-20 и реальными языковыми концепциями, созданными для Ranges TS и включенными в C ++ 20. Они оба называются «понятиями», но они определены по-другому. Таким образом, когда я говорю о concept-with-a-lowercase-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_iterators мог вернуть копии объекта сопоставления. Новые концепции позволяют работать таким образом, поддерживая прокси-итераторы.

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

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

4

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]