Czy można używać std :: transform z std :: back_inserter?

20

Cppreference ma ten przykładowy kod dla 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; });

Ale mówi również:

std::transformnie gwarantuje zastosowania unary_oplub binary_op. Aby zastosować funkcję do sekwencji w kolejności lub zastosować funkcję, która modyfikuje elementy sekwencji, użyj std::for_each.

Ma to prawdopodobnie umożliwić równoległe wdrożenia. Jednak trzecim parametrem std::transformjest LegacyOutputIteratornastępujący warunek ++r:

Po tej operacji rnie wymaga się, aby była inkrementowalna, a wszelkie kopie poprzedniej wartości rnie muszą już być dereferencyjne ani inkrementowalne.

Wydaje mi się więc, że przyporządkowanie danych wyjściowych musi nastąpić po kolei. Czy oznaczają one po prostu, że aplikacja unary_opmoże być nieczynna i przechowywana w tymczasowej lokalizacji, ale kopiowana na wyjście w odpowiedniej kolejności? To nie brzmi jak coś, co kiedykolwiek chciałbyś zrobić.

Większość bibliotek C ++ nie zaimplementowało jeszcze równoległych programów wykonawczych, ale Microsoft ma. Jestem prawie pewien, że jest to odpowiedni kod i myślę, że wywołuje populate()funkcję, aby rejestrować iteratory w porcjach danych wyjściowych, co z pewnością nie jest prawidłową rzeczą, ponieważ LegacyOutputIteratormoże zostać unieważnione przez zwiększenie jej kopii.

czego mi brakuje?

Timmmm
źródło
Prosty test w Godbolt pokazuje, że jest to problem. Z C ++ 20 i transformwersją, która decyduje, czy użyć paralelizmu. W transformprzypadku dużych wektorów zawodzi.
Croolman,
6
@Croolman Twój kod jest nieprawidłowy, ponieważ wstawiasz go z powrotem s, co unieważnia iteratory.
Daniel Langr,
@DanielsaysreinstateMonica Oh schnitzel masz rację. Poprawiałem go i pozostawiłem w nieważnym stanie. Cofam swój komentarz.
Croolman,
Jeśli korzystasz std::transformz zasad exaction, wymagany jest iterator o dostępie swobodnym, który back_inserternie może spełnić. Cytowana przez IMO dokumentacja części odnosi się do tego scenariusza. Uwaga przykład w zastosowaniach dokumentacji std::back_inserter.
Marek R
@Croolman Decyduje się na użycie równoległości automatycznie?
ciekawy,

Odpowiedzi:

9

1) Wymagania iteratora wyjściowego w normie są całkowicie zepsute. Zobacz LWG2035 .

2) Jeśli używasz iteratora czysto wyjściowego i zakresu wyłącznie wejściowego źródła, algorytm może zrobić niewiele więcej; nie ma wyboru, musi pisać w kolejności. (Jednak hipotetyczna implementacja może wybrać specjalne przypadki własnych typów, na przykład std::back_insert_iterator<std::vector<size_t>>; Nie rozumiem, dlaczego jakakolwiek implementacja chciałaby to zrobić tutaj, ale jest to dozwolone).

3) Nic w standardowych gwarancjach, które transformstosują transformacje w kolejności. Patrzymy na szczegół implementacji.

To std::transformwymaga tylko iteratorów wyjściowych, co nie oznacza, że ​​nie może wykryć wyższych sił iteratora i zmienić kolejność operacji w takich przypadkach. Rzeczywiście, algorytmy wysyłką na iteratora wytrzymałość przez cały czas , a oni mają specjalnego traktowania dla specjalnych typów iterator (jak wskazówki lub iteratory wektor) cały czas .

Kiedy standard chce zagwarantować określone zamówienie, wie, jak to powiedzieć (patrz std::copy„zaczynając od firsti przechodząc do last”).

TC
źródło
5

Od n4385:

§25.6.4 Przekształcenie :

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

Zwraca: back_insert_iterator (x).

§23.5.2.1 Szablon klasy back_insert_iterator

using iterator_category = output_iterator_tag;

Dlatego std::back_inserternie można go używać z równoległymi wersjami std::transform. Wersje obsługujące iteratory wyjściowe czytane ze źródła za pomocą iteratorów wejściowych. Od iteratory wejściowe mogą być tylko przed i po zwiększany (§23.3.5.2 iteratory wejściowe) i jest tylko kolejny ( tj nierównoległe) wykonanie, zlecenie musi być zachowana między nimi a iteratora wyjściowego.

Paul Evans
źródło
2
Należy zauważyć, że te definicje ze standardu C ++ nie unikają implementacji zapewniających specjalne wersje algorytmów wybranych dla dodatkowych typów iteratorów. Na przykład, std::advancema tylko jedną definicję, która pobiera input-iteratory , ale libstdc ++ zapewnia dodatkowe wersje dla dwukierunkowego iteratory i random-access-iteratorów . Konkretna wersja jest następnie wykonywana na podstawie typu przekazanego iteratora .
Daniel Langr,
Nie sądzę, aby twój komentarz był poprawny - ForwardIteratornie oznacza to, że musisz robić rzeczy po kolei. Ale podkreśliły co mi brakowało - dla równoległych wersjach z których korzystają ForwardIteratornie OutputIterator.
Timmmm,
1
Ach tak, myślę, że się zgadzamy.
Timmmm,
1
Ta odpowiedź mogłaby skorzystać z dodania kilku słów wyjaśniających, co to właściwie znaczy.
Barry
1
@Barry Dodano kilka słów, wszystkie bardzo mile widziane.
Paul Evans,
0

Więc tęskniłem za tym, że wersje równoległe wymagają LegacyForwardIterators, a nie LegacyOutputIterator. LegacyForwardIterator Może być zwiększany bez unieważniania kopie, więc łatwo jest wykorzystać do wdrożenia out-of-order równolegle std::transform.

Myślę, że nierównoległe wersje std::transform muszą być wykonywane w kolejności. Albo cppreferencja jest w tym zakresie błędna, albo być może standard po prostu pozostawia to wymaganie niejawne, ponieważ nie ma innego sposobu na jego wdrożenie. (Strzelba nie przedziera się przez standard, żeby się dowiedzieć!)

Timmmm
źródło
Nierównoległe wersje transformacji mogą być wykonywane poza kolejnością, jeśli wszystkie iteratory są wystarczająco silne. Na przykład w pytaniu nie są one, tak że specjalizacja z transformmusi być w prawidłowej kolejności.
Caleth,
Nie, nie mogą, bo LegacyOutputIteratorzmusza cię do używania go w kolejności.
Timmmm,
Może specjalizować się inaczej dla std::back_insert_iterator<std::vector<T>>i std::vector<T>::iterator. Pierwszy musi być w porządku. Drugi nie ma takiego ograniczenia
Caleth
Ach, czekaj, rozumiem, co masz na myśli - jeśli zdarzy się, że przekażesz LegacyForwardIteratornierównoległość transform, może ona mieć specjalizację dla tego, co robi to nie w porządku. Słuszna uwaga.
Timmmm,
0

Uważam, że transformacja jest gwarantowana w kolejności . std::back_inserter_iteratorjest iteratorem wyjściowym (jego iterator_categorytyp elementu jest aliasem std::output_iterator_tag) zgodnie z [back.insert.iterator] .

W związku z tym std::transformnie ma innej opcji, jak przejść do następnej iteracji, niż wywołać członka operator++w resultparametrze.

Oczywiście dotyczy to tylko przeciążeń bez zasad wykonywania, w których std::back_inserter_iteratornie można ich używać (nie jest to iterator przekazywania ).


BTW, nie argumentowałbym cytatami z cppreferencji. Stwierdzenia tam często są nieprecyzyjne lub uproszczone. W takich przypadkach lepiej spojrzeć na standard C ++. Tam, gdzie w odniesieniu do std::transform, nie ma cytatu o kolejności operacji.

Daniel Langr
źródło
„Standard C ++. Gdzie, w odniesieniu do std :: transform, nie ma cytatu na temat kolejności operacji” Skoro kolejność nie jest wymieniona, czy nie jest określona?
HolyBlackCat,
@HolyBlackCat Jawnie nieokreślony, ale narzucony przez iterator wyjściowy. Zauważ, że w przypadku iteratorów wyjściowych, po jego zwiększeniu, nie można odrzucić żadnej poprzedniej wartości iteratora.
Daniel Langr,