std :: back_inserterでstd :: transformを使用することは有効ですか?


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::transformunary_opまたはの順序どおりの適用は保証されませんbinary_op。関数をシーケンスに順番に適用したり、シーケンスの要素を変更する関数を適用したりするには、を使用しますstd::for_each

これはおそらく並列実装を可能にするためです。しかし、の3番目のパラメータは、std::transformあるLegacyOutputIteratorため、次の事後条件を有しています++r

この操作の後rは、増分可能である必要はなく、以前の値のコピーは、r逆参照可能または増分可能である必要がなくなります。

したがって、出力の割り当ては順番に行わなければならないように思えます。それらは単に、アプリケーションunary_opが故障し、一時的な場所に保存されているが、出力に順番にコピーされる可能性があることを意味していますか?それはあなたがしたいことのようには聞こえません。

ほとんどのC ++ライブラリは、実際には並列実行プログラムをまだ実装していませんが、Microsoftは実装しています。私はかなり確信している、これは関連するコードである、と私は考えてそれが呼び出すこのpopulate()機能をするので確実に行うために有効なものではありません出力、のチャンクにレコードイテレータに LegacyOutputIteratorそれのコピーをインクリメントすることによって無効にすることができます。

何が欠けていますか?


godboltでの簡単なテストは、これが問題であることを示しています。C ++ 20とtransform並列処理を使用するかどうかを決定するバージョン。transform大きなベクトルのために失敗します。
Croolman

6
@Croolmanに戻り挿入しているためs、コードが正しくありません。イテレータが無効になります。
Daniel Langr

@DanielsaysreinstateMonicaああシュニッツェルそうです。それを微調整し、無効な状態のままにしました。コメントを取り戻します。
Croolman

あなたが使用している場合はstd::transformexaction方針で、その後ランダムアクセスイテレータが必要とされている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::advanceinput-iteratorsをとる定義が1つしかありませんが、libstdc ++は双方向の反復子ランダムアクセスの反復子に追加のバージョンを提供します。次に、渡されたイテレータのタイプに基づいて特定のバージョンが実行されます
Daniel Langr

私はあなたのコメントが正しいとは思いません- ForwardIteratorだからといって、順番に行動しなければならないわけではありません。しかし、あなたが私が見逃したことを強調しました-彼らが使用しForwardIteratorない並列バージョンのためにOutputIterator
Timmmm

1
ええ、そうです、私たちは同意していると思います。
Timmmm

1
この答えは、それが実際に何を意味するかを説明するためにいくつかの単語を追加することから利益を得ることができます。
バリー

1
@Barryいくつかの単語を追加しました。ありとあらゆるフィードバックが高く評価されました。
Paul Evans

0

だから私が見逃したのは、並列バージョンはLegacyForwardIteratorsではなくsをとることLegacyOutputIteratorです。のコピーを無効にLegacyForwardIterator することなくA インクリメントできるので、これを使用してアウトオブオーダーのparallelを実装するのは簡単std::transformです。

の非並列バージョンは順序どおりに実行するstd::transform 必要があると思います。それについてcppreferenceが間違っているか、標準を実装する他の方法がないため、標準がこの要件を暗黙的に残している可能性があります。(ショットガンは、標準を調べて確認していません!)


すべての反復子が十分に強力な場合、変換の非並列バージョンは順不同で実行される可能性があります。問題の例ではそうはないので、の特殊化transform順序どおりでなければなりません。
Caleth

いいえ、そうではないかもしれません。なぜなら、LegacyOutputIteratorそれを順序どおりに使用する必要があるからです。
Timmmm

それはのために別々に特化することができますstd::back_insert_iterator<std::vector<T>>std::vector<T>::iterator。最初の順序でなければなりません。2番目にはそのような制限はありません
Caleth

ああ待って私はあなたが何を意味するのか見てみます-あなたがたまたまLegacyForwardIteratornon-parallel に渡した場合transform、それは順不同でそれを行うことに特化しているかもしれません。いい視点ね。
Timmmm

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明示的には指定されていませんが、出力反復子によって強制されています。出力イテレーターでは、一度インクリメントすると、以前のイテレーター値を逆参照できないことに注意してください。
Daniel Langr
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.