Допустимо ли использовать std :: transform с std :: back_inserter?

20

Cppreference имеет этот пример кода для std::transform:

std::vector<std::size_t> ordinals;
std::transform(s.begin(), s.end(), std::back_inserter(ordinals),
               [](unsigned char c) -> std::size_t { return c; });

Но это также говорит:

std::transformне гарантирует применение в порядке unary_opили binary_op. Чтобы применить функцию к последовательности по порядку или применить функцию, которая изменяет элементы последовательности, используйте std::for_each.

Предположительно, это допускает параллельные реализации. Однако третий параметр std::transformпредставляет собой LegacyOutputIteratorследующее условие ++r:

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

Поэтому мне кажется, что назначение вывода должно происходить по порядку. Означают ли они просто, что приложение unary_opможет быть не в порядке и сохранено во временном местоположении, но скопировано в выходные данные по порядку? Это не похоже на то, что вы когда-либо хотели бы сделать.

Большинство библиотек C ++ на самом деле еще не реализовали параллельных исполнителей, но Microsoft имеет. Я почти уверен, что это релевантный код, и я думаю, что он вызывает эту populate()функцию для записи итераторов в порции вывода, что, безусловно, не является допустимым действием, поскольку его LegacyOutputIteratorможно сделать недействительным, увеличив его копии.

Что мне не хватает?

Timmmm
источник
Простой тест в Godbolt показывает, что это проблема. С C ++ 20 и transformверсией, которая решает, использовать ли паралелизм или нет. Для transformбольших векторов не удается.
Крулман
6
@Croolman Ваш код неверен, так как вы вставляете обратно s, что делает итераторы недействительными.
Даниэль Лангр
@DanielsaysreinstateMonica О, шницель, ты прав. Дорабатывал и оставил в недействительном состоянии. Я забираю свой комментарий обратно.
Крулман
Если вы используете std::transformполитику exaction, то необходим итератор произвольного доступа, который back_inserterне может быть выполнен. ИМО цитирует часть документации, относящейся к этому сценарию. Обратите внимание на пример использования документации std::back_inserter.
Марек Р
@Croolman Решает использовать параллелизм автоматически?
любопытный парень

Ответы:

9

1) Требования к выходному итератору в стандарте полностью нарушены. Смотри LWG2035 .

2) Если вы используете чисто выходной итератор и чисто исходный диапазон входных данных, то на практике алгоритм мало что может сделать; у него нет другого выбора, кроме как писать по порядку. (Тем не менее, гипотетическая реализация может выбрать особый случай для своих собственных типов, например std::back_insert_iterator<std::vector<size_t>>; я не понимаю, почему любая реализация захотела бы сделать это здесь, но это разрешено делать.)

3) Ничто в стандарте не гарантирует, что transform применения преобразований по порядку. Мы смотрим на детали реализации.

Это std::transformтребует только выходных итераторов, но это не значит, что он не может обнаружить более сильные итераторы и переупорядочить операции в таких случаях. Действительно, алгоритмы отправки на итератора силы все время , и они имеют специальную обработку для специальных типов итераторов (например , указатели или векторных итераторов) все время .

Когда стандарт хочет гарантировать определенный заказ, он знает, как это сказать (см. std::copy«Начиная с firstи переходя к last»).

TC
источник
5

От n4385:

§25.6.4 Преобразование :

template<class InputIterator, class OutputIterator, class UnaryOperation>
constexpr OutputIterator
transform(InputIterator first1, InputIterator last1, OutputIterator result, UnaryOperation op);

template<class ExecutionPolicy, class ForwardIterator1, class ForwardIterator2, class UnaryOperation>
ForwardIterator2
transform(ExecutionPolicy&& exec, ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 result, UnaryOperation op);

template<class InputIterator1, class InputIterator2, class OutputIterator, class BinaryOperation>
constexpr OutputIterator
transform(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, OutputIterator result, BinaryOperation binary_op);

template<class ExecutionPolicy, class ForwardIterator1, class ForwardIterator2, class ForwardIterator, class BinaryOperation>
ForwardIterator
transform(ExecutionPolicy&& exec, ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator result, BinaryOperation binary_op);

§23.5.2.1.2 back_inserter

template<class Container>
constexpr back_insert_iterator<Container> back_inserter(Container& x);

Возвращает: back_insert_iterator (x).

§23.5.2.1 Шаблон класса back_insert_iterator

using iterator_category = output_iterator_tag;

Поэтому std::back_inserterнельзя использовать с параллельными версиями std::transform. Версии, которые поддерживают выходные итераторы, читают из их источника входные итераторы. Поскольку входные итераторы могут быть только до и после приращения (§23.3.5.2 Входные итераторы) и существует только последовательное ( то есть непараллельное) выполнение, порядок между ними и выходным итератором должен быть сохранен.

Пол Эванс
источник
2
Обратите внимание, что эти определения из Стандарта C ++ не избегают реализаций для предоставления специальных версий алгоритмов, выбранных для дополнительных типов итераторов. Например, std::advanceимеет только одно определение, которое принимает итераторы ввода , но libstdc ++ предоставляет дополнительные версии для двунаправленных итераторов и итераторов с произвольным доступом . Затем конкретная версия выполняется в зависимости от типа переданного итератора .
Даниэль Лангр
Я не думаю, что ваш комментарий правильный - ForwardIteratorэто не значит, что вы должны делать все по порядку. Но вы подчеркнули то, что я пропустил - для параллельных версий они ForwardIteratorне пользуются OutputIterator.
Тимммм
1
Ах да, я думаю, что мы согласны.
Тимммм
1
Этот ответ может быть полезным, добавив несколько слов, чтобы объяснить, что это на самом деле означает.
Барри
1
@Barry Добавил несколько слов, все отзывы очень ценятся.
Пол Эванс
0

Поэтому я упустил то, что параллельные версии берут LegacyForwardIterators, а не LegacyOutputIterator. Значение A LegacyForwardIterator можно увеличивать, не аннулируя его копии, поэтому его легко использовать для реализации неупорядоченной параллели std::transform.

Я думаю, что непараллельные версии std::transform должны быть выполнены в порядке. Либо cppreference ошибочен, либо, возможно, стандарт просто оставляет это требование неявным, потому что другого способа его реализации нет. (Дробовик не пробирается через стандарт, чтобы узнать!)

Timmmm
источник
Непараллельные версии преобразования могут выполняться не по порядку, если все итераторы достаточно сильны. В примере в этом вопросе они не являются, так что специализация в transformдолжна быть в порядке.
Caleth
Нет, они не могут, потому что LegacyOutputIteratorзаставляет вас использовать его по порядку.
Тимммм
Он может по-разному специализироваться на std::back_insert_iterator<std::vector<T>>и std::vector<T>::iterator. Первое должно быть в порядке. Второй не имеет такого ограничения
Caleth
Ах, подождите, я понимаю, что вы имеете в виду - если вам случится передать a LegacyForwardIteratorв непараллель transform, это может иметь специализацию для того, что делает это не по порядку. Хорошая точка зрения.
Тимммм
0

Я считаю, что преобразование гарантированно будет обработано по порядку . std::back_inserter_iteratorявляется выходным итератором (его iterator_categoryтип члена является псевдонимом для std::output_iterator_tag) в соответствии с [back.insert.iterator] .

Следовательно, std::transformне имеет никакого другого выбора , как перейти к следующей итерации , чем член вызова operator++по resultпараметру.

Конечно, это действительно только для перегрузок без политики выполнения, где std::back_inserter_iteratorих нельзя использовать (это не итератор пересылки ).


Кстати, я бы не стал спорить с цитатами из cppreference. Утверждения там часто неточны или упрощены. В таких случаях лучше взглянуть на стандарт C ++. Где, относительно std::transform, нет цитаты о порядке операций.

Даниэль Лангр
источник
«Стандарт C ++. Где, в отношении std :: transform, нет никакой цитаты о порядке операций» Поскольку порядок не указан, не указан ли он?
HolyBlackCat
@HolyBlackCat Явно не определено, но навязано итератором вывода. Обратите внимание, что с выходными итераторами, когда вы увеличиваете его, вы не можете разыменовывать любое предыдущее значение итератора.
Даниэль Лангр