Чи правильно використовувати std :: transform with 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є a, LegacyOutputIteratorякий має наступні умови для ++r:

Після цієї операції rне потрібно збільшувати дані, і будь-які копії попереднього значення rбільше не потрібно бути перенавантажуваними або наростатими.

Тому мені здається, що призначення результату має відбуватися по порядку. Чи просто вони означають, що додаток unary_opможе вийти з ладу і зберігатися у тимчасовому місці, але скопійовано у вихідний порядок? Це не схоже на те, що ти коли-небудь хотів би зробити.

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

Що я пропускаю?


Простий тест на богболт показує, що це проблема. З C ++ 20 та transformверсією, яка вирішує, використовувати паралелізм чи ні. Для transformвеликих векторів не вдається.
Croolman

6
@Croolman Ваш код невірний, оскільки ви повертаєтесь назад s, що скасовує ітератори.
Даніель Лангр

@DanielsaysreinstateMonica О, шніцель, ти маєш рацію. Налаштував його та залишив у недійсному стані. Я беру свій коментар назад.
Croolman

Якщо ви використовуєте std::transformполітику вимогливості, потрібен ітератор випадкового доступу, який back_inserterнеможливо виконати. Документація, що цитується IMO, стосується цього сценарію. Зверніть увагу на приклад використання документації std::back_inserter.
Marek R

@Croolman Вирішив автоматично використовувати паралелізм?
curiousguy

Відповіді:


9

1) Вимоги до стандарту ітератора виводу в стандарті повністю порушені. Див. LWG2035 .

2) Якщо ви використовуєте суто ітератор виводу та суто діапазон вхідного джерела, алгоритм ще мало може зробити на практиці; у нього немає іншого вибору, як писати по порядку. (Однак гіпотетична реалізація може вибрати спеціальні регістри власних типів, наприклад std::back_insert_iterator<std::vector<size_t>>; я не бачу, чому будь-яка реалізація хотіла б це зробити тут, але це дозволено.)

3) Ніщо в стандарті не гарантує цього transform застосовують перетворення в порядку. Ми розглядаємо деталі реалізації.

Це std::transformвимагає лише ітераторів виводу, не означає, що він не може виявити більш високу потужність ітератора та змінити операції в таких випадках. Дійсно, алгоритми відправки на ітератора сили весь час , і вони мають спеціальну обробку для спеціальних типів ітераторів (наприклад , покажчики або векторних ітераторів) весь час .

Коли стандарт хоче гарантувати певне замовлення, він знає, як це сказати (див std::copy. "Починаючи з firstта продовжуючи last").


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.
Timmmm

1
Ага, так, так, я думаю, що ми згодні.
Timmmm

1
Ця відповідь могла б отримати користь від додавання деяких слів, щоб пояснити, що це насправді означає.
Баррі

1
@Barry Додав кілька слів, будь-які і всі відгуки високо оцінені.
Пол Еванс

0

Тому я пропустив те, що паралельні версії беруть LegacyForwardIterator, а не LegacyOutputIterator. А LegacyForwardIterator може бути збільшений без недійсних копій його, тому його легко використовувати для здійснення паралелі поза порядкомstd::transform .

Я думаю, що непаралельні версії std::transform повинні бути виконані в порядку. Або cppreference в цьому неправильно, або, можливо, стандарт просто залишає цю вимогу неявною, оскільки іншого способу її реалізації не існує. (Рушниця не пробирається через стандарт, щоб дізнатися!)


Непаралельні версії перетворення можуть виконуватись поза порядком, якщо всі ітератори досить сильні. У прикладі в цьому питанні вони не є, так що спеціалізація в transformповинна бути в порядку.
Калет

Ні, вони можуть, тому що вони LegacyOutputIteratorзмушують вас використовувати це в порядку.
Timmmm

Він може спеціалізуватися по-різному для std::back_insert_iterator<std::vector<T>>та std::vector<T>::iterator. Перший повинен бути в порядку. Другий не має такого обмеження
Калет

Ну зачекайте, я розумію, що ви маєте на увазі - якщо вам трапиться перейти в 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 Явно не визначено, але накладається ітератором виводу. Зверніть увагу, що з ітераторами виведення, після збільшення їх кількості, ви можете не скидати жодне попереднє значення ітератора.
Даніель Лангр
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.