Apakah valid menggunakan std :: transform with std :: back_inserter?

20

Cppreference memiliki kode contoh ini untuk 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; });

Tetapi juga dikatakan:

std::transformtidak menjamin aplikasi unary_opatau binary_op. Untuk menerapkan fungsi ke urutan secara berurutan atau menerapkan fungsi yang memodifikasi elemen urutan, gunakan std::for_each.

Ini mungkin untuk memungkinkan implementasi paralel. Namun parameter ketiga std::transformadalah LegacyOutputIteratoryang memiliki postcondition berikut untuk ++r:

Setelah operasi rini tidak perlu bertambah dan salinan dari nilai sebelumnya rtidak lagi diperlukan untuk dereferensi atau bertambah.

Jadi menurut saya penugasan output harus dilakukan secara berurutan. Apakah mereka hanya berarti bahwa aplikasi unary_opmungkin rusak, dan disimpan ke lokasi sementara, tetapi disalin ke output secara berurutan? Itu tidak terdengar seperti sesuatu yang ingin Anda lakukan.

Kebanyakan pustaka C ++ belum benar-benar mengimplementasikan eksekutor paralel, tetapi Microsoft telah melakukannya. Saya cukup yakin ini adalah kode yang relevan, dan saya pikir ini memanggil fungsi inipopulate() untuk merekam iterator ke potongan output, yang tentunya bukan hal yang valid untuk dilakukan karena LegacyOutputIteratordapat dibatalkan dengan menambah salinannya.

Apa yang saya lewatkan?

Timmmm
sumber
Sebuah tes sederhana di godbolt menunjukkan ini adalah masalah. Dengan C ++ 20 dan transformversi yang memutuskan apakah akan menggunakan paralelisme atau tidak. The transformuntuk vektor besar gagal.
Croolman
6
@Cololman Kode Anda salah, karena Anda memasukkan kembali ke s, yang membatalkan iterator.
Daniel Langr
@DanielsaysreinstateMonica Oh schnitzel kau benar. Tweak itu dan meninggalkannya dalam keadaan tidak valid. Saya mengambil komentar saya kembali.
Croolman
Jika Anda menggunakan std::transformdengan kebijakan exaction, diperlukan iterator akses acak yang back_insertertidak dapat dipenuhi. Dokumentasi bagian yang dikutip IMO mengacu pada skenario itu. Perhatikan contoh dalam penggunaan dokumentasi std::back_inserter.
Marek R
@Croolman Memutuskan untuk menggunakan paralelisme secara otomatis?
curiousguy

Jawaban:

9

1) Persyaratan iterator keluaran dalam standar benar-benar rusak. Lihat LWG2035 .

2) Jika Anda menggunakan iterator keluaran murni dan rentang sumber input murni, maka tidak banyak lagi yang dapat dilakukan algoritma dalam praktiknya; ia tidak punya pilihan selain menulis secara berurutan. (Namun, implementasi hipotetis dapat memilih untuk tipe kasus khusus sendiri, seperti std::back_insert_iterator<std::vector<size_t>>; Saya tidak melihat mengapa implementasi apa pun ingin melakukannya di sini, tetapi diizinkan untuk melakukannya.)

3) Tidak ada dalam jaminan standar yang transformmenerapkan transformasi secara berurutan. Kami melihat detail implementasi.

Itu std::transformhanya membutuhkan iterator keluaran tidak berarti tidak dapat mendeteksi kekuatan iterator yang lebih tinggi dan menyusun ulang operasi dalam kasus tersebut. Memang, algoritma mengirimkan kekuatan iterator sepanjang waktu , dan mereka memiliki penanganan khusus untuk tipe iterator khusus (seperti pointer atau iterator vektor) sepanjang waktu .

Ketika standar ingin menjamin pesanan tertentu, ia tahu bagaimana mengatakannya (lihat std::copy"mulai dari firstdan melanjutkan ke last").

TC
sumber
5

Dari n4385:

§25.6.4 Transformasi :

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);

Pengembalian: back_insert_iterator (x).

§23.5.2.1 Template kelas back_insert_iterator

using iterator_category = output_iterator_tag;

Jadi std::back_insertertidak bisa digunakan dengan versi paralel std::transform. Versi yang mendukung iterator keluaran membaca dari sumbernya dengan iterator input. Karena iterator input hanya dapat sebelum dan sesudah kenaikan (§23.3.5.2 iterator input) dan hanya ada eksekusi berurutan ( yaitu non-paralel), urutan harus dipertahankan antara mereka dan iterator output.

Paul Evans
sumber
2
Perhatikan bahwa definisi ini dari Standar C ++ tidak menghindari implementasi untuk menyediakan versi khusus dari algoritma yang dipilih untuk jenis iterator tambahan. Misalnya, std::advancehanya memiliki satu definisi yang mengambil input-iterator , tetapi libstdc ++ menyediakan versi tambahan untuk bidirectional-iterators dan random-access-iterators . Versi tertentu kemudian dieksekusi berdasarkan jenis iterator yang diteruskan .
Daniel Langr
Saya tidak berpikir komentar Anda benar - ForwardIteratorbukan berarti Anda harus melakukan sesuatu secara berurutan. Tetapi Anda telah menyoroti hal yang saya lewatkan - untuk versi paralel yang mereka gunakan ForwardIteratortidak OutputIterator.
Timmmm
1
Ah benar, ya saya pikir kita setuju.
Timmmm
1
Jawaban ini dapat bermanfaat dengan menambahkan beberapa kata untuk menjelaskan apa artinya sebenarnya.
Barry
1
@Barry Menambahkan beberapa kata, setiap dan semua umpan balik sangat dihargai.
Paul Evans
0

Jadi hal yang saya lewatkan adalah bahwa versi paralel mengambil LegacyForwardIterators, bukan LegacyOutputIterator. A LegacyForwardIterator dapat ditambahkan tanpa membuat salinannya tidak valid, sehingga mudah digunakan untuk mengimplementasikan paralel yang tidak sesuai pesanan std::transform.

Saya pikir versi non-paralel std::transform harus dijalankan secara berurutan. Entah cppreference salah tentangnya, atau mungkin standar membiarkan persyaratan ini implisit karena tidak ada cara lain untuk mengimplementasikannya. (Shotgun tidak mengarungi standar untuk mencari tahu!)

Timmmm
sumber
Versi-versi non paralel dari transformasi dapat berjalan di luar urutan jika semua iterator cukup kuat. Dalam contoh dalam pertanyaan mereka tidak, sehingga bahwa spesialisasi dari transformharus di-order.
Caleth
Tidak, mereka mungkin tidak melakukannya, karena LegacyOutputIteratormemaksa Anda untuk menggunakannya secara berurutan.
Timmmm
Ini dapat mengkhususkan secara berbeda untuk std::back_insert_iterator<std::vector<T>>dan std::vector<T>::iterator. Yang pertama harus berurutan. Yang kedua tidak memiliki batasan seperti itu
Caleth
Ah, tunggu, saya mengerti apa yang Anda maksudkan - jika Anda kebetulan melewatinya secara LegacyForwardIteratornon-paralel transform, itu bisa memiliki spesialisasi untuk hal yang tidak sesuai aturan . Poin bagus.
Timmmm
0

Saya percaya transformasi dijamin diproses secara berurutan . std::back_inserter_iteratoradalah output iterator ( iterator_categorytipe anggotanya adalah alias untuk std::output_iterator_tag) menurut [back.insert.iterator] .

Akibatnya, std::transformmemiliki pilihan lain tentang bagaimana untuk melanjutkan ke iterasi berikutnya daripada anggota panggilan operator++pada resultparameter.

Tentu saja, ini hanya berlaku untuk kelebihan beban tanpa kebijakan eksekusi, di mana std::back_inserter_iteratormungkin tidak digunakan (ini bukan iterator penerusan ).


BTW, saya tidak akan berdebat dengan kutipan dari cppreference. Pernyataan di sana sering tidak tepat atau disederhanakan. Dalam hal ini, lebih baik untuk melihat Standar C ++. Di mana, berkenaan dengan std::transform, tidak ada kutipan tentang urutan operasi.

Daniel Langr
sumber
"Standar C ++. Di mana, mengenai std :: transform, tidak ada kutipan tentang urutan operasi" Karena urutan tidak disebutkan, bukankah tidak ditentukan?
HolyBlackCat
@HolyBlackCat Secara eksplisit tidak ditentukan, tetapi dipaksakan oleh iterator keluaran. Perhatikan bahwa dengan iterator keluaran, setelah Anda menambahkannya, Anda tidak boleh melakukan dereferensi nilai iterator sebelumnya.
Daniel Langr