Что здесь происходит?
Вся проблема здесь заключается в том, что std::max_element
требует, чтобы его аргументы были LecacyForwardIterators , в то время как диапазоны, созданные ranges::v3::yield
, очевидно (очевидно?) Предоставляют только LecacyInputIterators . К сожалению, в документах range-v3 явно не упоминаются категории итераторов, которых можно ожидать (по крайней мере, я не обнаружил, что они упоминались). Это действительно было бы огромным улучшением, поскольку все стандартные библиотечные алгоритмы явно указывают, какие категории итераторов им требуются.
В конкретном случае std::max_element
вы не первый, кто наткнулся на это противоречащее интуиции требование ForwardIterator
, а не просто InputIterator
, см. Почему для std :: max_element требуется ForwardIterator? например. Таким образом, это имеет смысл, поскольку std::max_element
не (несмотря на название, предлагающее его) возвращает элемент max, но итератор элемента max. Следовательно, в частности, многопроходная гарантия отсутствует на InputIterator
, чтобы заставить std::max_element
работать с ней.
По этой причине многие другие стандартные библиотечные функции также не работают с std::max_element
, например, std :: istreambuf_iterator , что очень жаль: вы просто не можете получить элемент max из файла с существующей стандартной библиотекой! Вам сначала нужно загрузить весь файл в память, либо использовать собственный алгоритм max.
В стандартной библиотеке просто отсутствует алгоритм, который действительно возвращает элемент max, а не итератор, указывающий на элемент max. Такой алгоритм может работать и с InputIterator
s. Конечно, это может быть очень легко реализовано вручную, но все же было бы удобно иметь это в стандартной библиотеке. Я могу только догадываться, почему этого не существует. Возможно, одна из причин заключается в том, что value_type
требует возможности создания копии, поскольку InputIterator
не требуется для возврата ссылок на элементы, и это может, в свою очередь, быть нелогичным, если алгоритм max создает копию ...
Итак, теперь относительно ваших актуальных вопросов:
Почему это? (т.е. почему ваш диапазон возвращает только InputIterator
с?)
Очевидно, yield
создает значения на лету. Это сделано специально, это одна из причин, по которой нужно использовать yield: не нужно создавать (и, следовательно, хранить) диапазон заранее. Следовательно, я не понимаю, как yield может быть реализован таким образом, чтобы он отвечал многопроходной гарантии , особенно вторая пуля вызывает у меня головную боль:
- Если a и b сравниваются равными (a == b контекстуально преобразуется в true), то либо они оба не разыменовываются, либо * a и * b являются ссылками, связанными с одним и тем же объектом
Технически, я мог бы представить, что можно реализовать yield
таким образом, чтобы все итераторы, созданные из одного диапазона, совместно использовали общее внутреннее хранилище, которое заполнялось на лету во время первого обхода. Тогда разные итераторы могут дать вам одинаковые ссылки на базовые объекты. Но тогда std::max_element
будет молча потреблять O(n²)
памяти (все элементы вашего декартового произведения). Так что, на мой взгляд, определенно лучше этого не делать, а вместо этого заставлять пользователей материализовать диапазон самостоятельно, чтобы они знали о том, что это происходит.
Как мне избежать этого?
Что ж, как уже сказал metalfox, вы можете скопировать ваш вид, что приведет к различным диапазонам и, следовательно, к независимым итераторам. Тем не менее, это не заставит std::max_element
работать. Итак, учитывая природу yield
, ответ на этот вопрос, к сожалению, таков: вы просто не можете избежать этого с помощью yield
или любой другой техники, которая создает ценности на лету.
Как сохранить несколько независимых итераторов, указывающих в различных местах диапазона?
Это связано с предыдущим вопросом. В основном, этот вопрос отвечает сам себе: если вы хотите указать независимых итераторов в различных местоположениях , эти местоположения должны существовать где-то в памяти. Итак, вам нужно материализовать, по крайней мере, те элементы, у которых когда-то был итератор, указывающий на них, что в случае std::max_element
означает, что вам нужно материализовать все из них.
Должен ли я реализовывать декартово произведение другим способом?
Я могу представить много разных реализаций. Но ни один из них не сможет предоставить оба этих свойства вместе:
- возврат
ForwardIterator
с
- требуется меньше
O(n²)
памяти
Технически, можно реализовать итератор, специализированный для использования с std::max_element
, что означает, что он хранит в памяти только текущий максимальный элемент, чтобы на него можно было ссылаться ... Но это было бы несколько смешно не так ли? Мы не можем ожидать, что библиотека общего назначения, такая как range-v3, предложит такие узкоспециализированные категории итераторов.
Резюме
Вы говорите
В конце концов, я не думаю, что мой вариант использования такой редкий выброс и диапазоны
планируется добавить в стандарт C ++ 20 - так что должно быть
какой-то разумный способ достичь этого без ловушек ...
Я определенно согласен с тем, что "это не редкий выброс"! Однако это не обязательно означает, что «должен быть какой-то разумный способ достичь этого без ловушек». Рассмотрим, например, NP-hard проблемы. Это не редкий выброс, чтобы столкнуться с тем. Тем не менее, невозможно (если P = NP) решить их за полиномиальное время. И в вашем случае просто невозможно использовать std::max_element
без ForwardIterator
s. И невозможно реализовать ForwardIterator
(как определено стандартной библиотекой) на декартовом произведении без использования O(n²)
памяти.
Для частного случая std::max_element
я бы предложил реализовать собственную версию, которая возвращает элемент max , а не итератор, указывающий на него.
Однако, если я правильно понимаю ваш вопрос, ваше беспокойство носит более общий характер, и std::max_element
является лишь примером. Итак, я должен разочаровать вас. Даже с существующей стандартной библиотекой некоторые тривиальные вещи невозможны из-за несовместимых категорий итераторов (опять же, std::istreambuf_iterator
является существующим примером). Итак, если добавится range-v3, таких примеров просто будет еще несколько.
Итак, наконец, моя рекомендация состоит в том, чтобы просто использовать свои собственные алгоритмы, если это возможно, и проглотить таблетку материализации представления в противном случае.