Có hợp lệ để sử dụng std :: Transform với std :: back_inserter không?


20

Cppreference có mã ví dụ này cho 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; });

Nhưng nó cũng nói:

std::transformkhông đảm bảo ứng dụng theo thứ tự của unary_ophoặc binary_op. Để áp dụng một chức năng cho một thứ tự theo thứ tự hoặc để áp dụng một chức năng sửa đổi các yếu tố của một chuỗi, sử dụngstd::for_each .

Điều này có lẽ là để cho phép thực hiện song song. Tuy nhiên, tham số thứ ba của std::transformlà một LegacyOutputIteratortrong đó có các điều kiện sau cho ++r:

Sau thao tác rnày không bắt buộc phải tăng lên và bất kỳ bản sao nào của giá trị trước đó rkhông còn được yêu cầu để có thể hủy bỏ hoặc tăng thêm.

Vì vậy, dường như việc gán đầu ra phải diễn ra theo thứ tự. Có phải chúng chỉ đơn giản có nghĩa là ứng dụng unary_opcó thể bị lỗi và được lưu trữ đến một vị trí tạm thời, nhưng được sao chép vào đầu ra theo thứ tự? Điều đó không giống như những gì bạn muốn làm.

Hầu hết các thư viện C ++ chưa thực sự triển khai các trình thực thi song song, nhưng Microsoft thì có. Tôi khá chắc chắn đây là mã có liên quan và tôi nghĩ rằng nó gọi hàm nàypopulate() để ghi lại các vòng lặp thành khối của đầu ra, điều này chắc chắn không phải là điều hợp lệ để làm vì LegacyOutputIteratorcó thể bị vô hiệu hóa bằng cách tăng các bản sao của nó.

Tôi đang thiếu gì?


Một thử nghiệm đơn giản trong godbolt cho thấy đây là một vấn đề. Với C ++ 20 và transformphiên bản quyết định có sử dụng paralelism hay không. Các transformvectơ lớn thất bại.
Croolman

6
@Croolman Mã của bạn không chính xác, vì bạn đang chèn ngược trở lại s, điều này làm mất hiệu lực các trình vòng lặp.
Daniel Langr

@DanielsaysreinstateMonica Oh schnitzel bạn nói đúng. Đã điều chỉnh nó và để nó ở trạng thái không hợp lệ. Tôi nhận xét lại.
Croolman

Nếu bạn sử dụng std::transformvới chính sách đòi hỏi quá đáng rồi truy cập ngẫu nhiên iterator là cần thiếtback_inserterkhông thể thực hiện. IMO trích dẫn một phần tài liệu đề cập đến kịch bản đó. Lưu ý ví dụ trong tài liệu sử dụng std::back_inserter.
Marek R

@Croolman Quyết định sử dụng song song tự động?
tò mò

Câu trả lời:


9

1) Các yêu cầu lặp đầu ra trong tiêu chuẩn bị phá vỡ hoàn toàn. Xem LWG2035 .

2) Nếu bạn sử dụng một trình vòng lặp đầu ra hoàn toàn và một phạm vi nguồn đầu vào thuần túy, thì có rất ít thuật toán có thể làm trong thực tế; nó không có lựa chọn nào khác ngoài việc viết theo thứ tự. (Tuy nhiên, một thực hiện giả thuyết có thể chọn để đặc biệt hợp cụ thể loại riêng của mình, giống như std::back_insert_iterator<std::vector<size_t>>, tôi không hiểu tại sao bất kỳ thực hiện sẽ muốn làm điều đó ở đây, nhưng nó được phép làm như vậy.)

3) Không có gì trong tiêu chuẩn đảm bảo rằng transform áp dụng các phép biến đổi theo thứ tự. Chúng tôi đang xem xét một chi tiết thực hiện.

Điều đó std::transformchỉ yêu cầu các trình lặp đầu ra không có nghĩa là nó không thể phát hiện các cường độ lặp cao hơn và sắp xếp lại các hoạt động trong các trường hợp đó. Trên thực tế, các thuật toán cử vào sức mạnh iterator tất cả thời gian , và họ có xử lý đặc biệt với nhiều loại iterator đặc biệt (như con trỏ hoặc lặp vector) tất cả các thời gian .

Khi tiêu chuẩn muốn đảm bảo một đơn đặt hàng cụ thể, nó sẽ biết cách nói (xem std::copy"bắt đầu từ firstvà tiếp tục last").


5

Từ n4385 :

§25.6.4 Biến đổi :

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

Trả về: back_insert_iterator (x).

§23.5.2.1 Mẫu lớp back_insert_iterator

using iterator_category = output_iterator_tag;

Vì vậy, std::back_inserterkhông thể được sử dụng với các phiên bản song song của std::transform. Các phiên bản hỗ trợ các trình vòng lặp đầu ra đọc từ nguồn của chúng với các trình vòng lặp đầu vào. Kể từ khi vòng lặp đầu vào chỉ có thể là trước và sau tăng lên (§23.3.5.2 lặp Input) và chỉ có tuần tự ( tức là không song song) thực hiện, trật tự phải được bảo tồn giữa họ và iterator đầu ra.


2
Lưu ý rằng các định nghĩa từ Tiêu chuẩn C ++ không tránh việc triển khai để cung cấp các phiên bản thuật toán đặc biệt được chọn cho các loại trình lặp bổ sung. Ví dụ, std::advancechỉ có một định nghĩa mà mất đầu vào-lặp , nhưng libstdc ++ cung cấp các phiên bản bổ sung cho hai chiều-lặptruy cập ngẫu nhiên-lặp . Phiên bản cụ thể sau đó được thực thi dựa trên loại trình vòng lặp được thông qua .
Daniel Langr

Tôi không nghĩ nhận xét của bạn là đúng - ForwardIteratorkhông có nghĩa là bạn phải làm mọi thứ theo thứ tự. Nhưng bạn đã nhấn mạnh điều tôi đã bỏ lỡ - đối với các phiên bản song song họ ForwardIteratorkhông sử dụng OutputIterator.
Timmmm

1
À đúng rồi, tôi nghĩ chúng ta đồng ý.
Timmmm

1
Câu trả lời này có thể có lợi từ việc thêm một số từ để giải thích ý nghĩa thực sự của nó.
Barry

1
@Barry Đã thêm một số từ, bất kỳ và tất cả phản hồi đánh giá cao nhiều.
Paul Evans

0

Vì vậy, điều tôi bỏ lỡ là các phiên bản song song mất LegacyForwardIterators, không LegacyOutputIterator. A LegacyForwardIterator có thể được tăng lên mà không làm mất hiệu lực các bản sao của nó, vì vậy thật dễ dàng sử dụng điều này để thực hiện song song không theo thứ tự std::transform.

Tôi nghĩ rằng các phiên bản không song song của std::transform phải được thực hiện theo thứ tự. Hoặc cppreference là sai về nó, hoặc có thể tiêu chuẩn chỉ để lại yêu cầu này bởi vì không có cách nào khác để thực hiện nó. (Shotgun không lội qua tiêu chuẩn để tìm hiểu!)


Các phiên bản biến đổi không song song có thể thực hiện không theo thứ tự nếu tất cả các trình lặp đều đủ mạnh. Trong ví dụ trong câu hỏi họ không, vì vậy mà chuyên môn của transformphải đặt hàng.
Caleth

Không, họ có thể không, bởi vì LegacyOutputIteratorbuộc bạn phải sử dụng nó theo thứ tự.
Timmmm

Nó có thể chuyên khác nhau cho std::back_insert_iterator<std::vector<T>>std::vector<T>::iterator. Đầu tiên phải theo thứ tự. Thứ hai không có hạn chế như vậy
Caleth

Đợi đã, tôi hiểu ý của bạn - nếu bạn tình cờ chuyển một LegacyForwardIteratorthứ không song song transform, nó có thể có một chuyên môn cho cái mà nó không theo thứ tự. Điểm tốt.
Timmmm

0

Tôi tin rằng việc chuyển đổi được đảm bảo được xử lý theo thứ tự . std::back_inserter_iteratorlà một trình vòng lặp đầu ra ( iterator_categoryloại thành viên của nó là bí danh cho std::output_iterator_tag) theo [back.insert.iterator] .

Do đó, std::transformkhông có lựa chọn nào khác về cách tiến hành để lặp tiếp theo hơn là thành viên cuộc gọi operator++trênresult tham số.

Tất nhiên, điều này chỉ hợp lệ cho các trường hợp quá tải mà không có chính sách thực thi, trong đó std::back_inserter_iteratorcó thể không được sử dụng (nó không phải là trình lặp chuyển tiếp ).


BTW, tôi sẽ không tranh luận với trích dẫn từ cppreference. Các tuyên bố thường không chính xác hoặc đơn giản hóa. Trong trường hợp như vậy, tốt hơn là xem Tiêu chuẩn C ++. Ở đâu, liên quan đến std::transform, không có trích dẫn về thứ tự hoạt động.


"Tiêu chuẩn C ++. Ở đâu, liên quan đến std :: Transform, không có trích dẫn nào về thứ tự hoạt động" Vì đơn hàng không được đề cập, phải chăng nó không được chỉ định?
HolyBlackCat

@HolyBlackCat Hoàn toàn không xác định, nhưng được áp đặt bởi trình lặp đầu ra. Lưu ý rằng với các trình vòng lặp đầu ra, một khi bạn tăng nó, bạn không thể hủy bỏ bất kỳ giá trị lặp nào trước đó.
Daniel Langr
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.