Я пытался реализовать массив nD, как контейнер. Что-то, что обернет базовый контейнер последовательности и позволит обработать его как контейнер контейнеров (of …): arr[i][j][k]
должен быть (в конце концов, постоянным) ссылкой на _arr[(((i * dim2) + j) * dim3) + k]
,
Хорошо, пока нет, arr[i]
просто должен быть классом-оболочкой над подмассивом …
И когда я попытался реализовать интеграторы, я внезапно понял, что драконы повсюду:
operator []
возвращает прокси или оболочку вместо истинной ссылки (Когда контейнер не является контейнером?)Реальная проблема заключается в том, что, как только у вас есть прокси-контейнер, ни один итератор не сможет выполнить следующее требование для прямого итератора:
Прямые итераторы [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
)
Хорошо, у нас есть две похожие, но разные концепции. Итак, давайте выложим их.
Но сначала мне нужно провести различие между именованными требованиями 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_iterator
s мог вернуть копии объекта сопоставления. Новые концепции позволяют работать таким образом, поддерживая прокси-итераторы.
Теперь итераторы-хранилища полезны для кеширования; итераторы могут кэшировать свои результаты так, чтобы повторяться *it
использование только делает дорогую операцию один раз. Но помните о проблеме с засыпкой итераторов: возвращая ссылку на их содержимое. Ты не необходимость сделать это просто, чтобы получить кеширование. Вы можете кэшировать результаты в optional<T>
(который вы аннулируете, когда итератор находится в / уменьшен). Таким образом, вы все еще можете вернуть значение. Может потребоваться дополнительная копия, но reference
не должно быть сложным типом.
Конечно, все это означает, что auto &val = *it;
больше не является юридическим кодом Тем не мение, auto &&val = *it;
всегда будет работать На самом деле это большая часть версии итераторов Range TS.
Других решений пока нет …