È valido usare std :: transform con std :: back_inserter?


20

Cppreference ha questo codice di esempio per 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; });

Ma dice anche:

std::transformnon garantisce l'applicazione in ordine di unary_opo binary_op. Per applicare una funzione a una sequenza in ordine o per applicare una funzione che modifica gli elementi di una sequenza, utilizzare std::for_each.

Ciò è presumibilmente per consentire implementazioni parallele. Tuttavia, il terzo parametro di std::transformè a LegacyOutputIteratorche ha la seguente postcondizione per ++r:

Dopo questa operazione rnon è necessario essere incrementabili e le copie del valore precedente di rnon devono più essere dichiarabili o incrementabili.

Quindi mi sembra che l'assegnazione dell'output debba avvenire in ordine. Significa semplicemente che l'applicazione di unary_oppotrebbe essere fuori servizio e archiviata in una posizione temporanea, ma copiata nell'output in ordine? Non sembra qualcosa che vorresti mai fare.

La maggior parte delle librerie C ++ non ha ancora implementato gli esecutori paralleli, ma Microsoft ha. Sono abbastanza sicuro che questo sia il codice pertinente e penso che chiama questa populate()funzione per registrare iteratori in blocchi dell'output, che sicuramente non è una cosa valida da fare perché LegacyOutputIteratorpuò essere invalidato incrementandone le copie.

Cosa mi sto perdendo?


Un semplice test in godbolt mostra che questo è un problema. Con C ++ 20 e transformversione che decide se usare o meno il paralelismo. Il transformper vettori di grandi dimensioni ha esito negativo.
Croolman,

6
@Croolman Il tuo codice non è corretto, poiché stai reinserendo nuovamente in s, il che invalida gli iteratori.
Daniel Langr,

@DanielsaysreinstateMonica Oh schnitzel hai ragione. Lo stava modificando e lo ha lasciato in uno stato non valido. Riprendo il mio commento.
Croolman,

Se si utilizza std::transformcon criteri di esazione, è necessario un iteratore ad accesso casuale che back_inserternon può essere eseguito. La documentazione della parte quotata IMO si riferisce a quello scenario. Nota esempio nell'uso della documentazione std::back_inserter.
Marek R

@Croolman Decide di utilizzare il parallelismo automaticamente?
curiousguy

Risposte:


9

1) I requisiti dell'iteratore di output nello standard sono completamente rotti. Vedi LWG2035 .

2) Se si utilizza un iteratore di output puramente e un intervallo di sorgenti di input puramente, in pratica l'algoritmo può fare ben poco; non ha altra scelta che scrivere in ordine. (Tuttavia, un'implementazione ipotetica può scegliere in casi speciali i propri tipi, come std::back_insert_iterator<std::vector<size_t>>; Non vedo perché qualsiasi implementazione vorrebbe farlo qui, ma è permesso farlo.)

3) Nulla nella garanzia standard transformapplica le trasformazioni in ordine. Stiamo esaminando un dettaglio di implementazione.

Ciò std::transformrichiede solo gli iteratori di output non significa che non è in grado di rilevare livelli di iteratore più elevati e riordinare le operazioni in tali casi. In effetti, gli algoritmi di spedizione su iteratore forza per tutto il tempo , e hanno un trattamento speciale per i tipi di iteratori speciali (come i puntatori o iteratori vettoriali) per tutto il tempo .

Quando lo standard vuole garantire un ordine particolare, sa come dirlo (vedi std::copy"iniziare da firste procedere a last").


5

Da n4385:

§25.6.4 Trasforma :

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

Restituisce: back_insert_iterator (x).

§23.5.2.1 Modello di classe back_insert_iterator

using iterator_category = output_iterator_tag;

Quindi std::back_inserternon può essere utilizzato con versioni parallele di std::transform. Le versioni che supportano gli iteratori di output leggono dalla loro sorgente con iteratori di input. Poiché gli iteratori di input possono essere solo pre e post-incrementati (§23.3.5.2 Iteratori di input) e esiste solo l' esecuzione sequenziale ( cioè non parallela), l'ordine deve essere preservato tra loro e l'iteratore di output.


2
Si noti che queste definizioni dallo standard C ++ non evitano le implementazioni per fornire versioni speciali di algoritmi che sono selezionati per altri tipi di iteratori. Per esempio, std::advanceha una sola definizione che prende in ingresso-iteratori , ma libstdc ++ fornisce ulteriori versioni per bidirezionali-iteratori e ad accesso casuale iteratori . La versione particolare viene quindi eseguita in base al tipo di iteratore passato .
Daniel Langr,

Non penso che il tuo commento sia corretto - ForwardIterators non significa che devi fare le cose in ordine. Ma si è messo in evidenza la cosa che mi mancava - per le versioni parallele che utilizzano ForwardIteratornon OutputIterator.
Timmmm,

1
Ah giusto, sì, penso che siamo d'accordo.
Timmmm,

1
Questa risposta potrebbe trarre vantaggio dall'aggiunta di alcune parole per spiegare cosa significa effettivamente.
Barry,

1
@Barry Aggiunte alcune parole, ogni feed back è molto apprezzato.
Paul Evans,

0

Quindi la cosa che mi è sfuggita è che le versioni parallele prendono LegacyForwardIterators, no LegacyOutputIterator. A LegacyForwardIterator può essere incrementato senza invalidarne le copie, quindi è facile utilizzarlo per implementare un parallelo fuori servizio std::transform.

Penso che le versioni non parallele di std::transform debbano essere eseguite in ordine. O cppreference è sbagliato al riguardo, o forse lo standard lascia implicito questo requisito perché non c'è altro modo per implementarlo. (Il fucile non guadagna lo standard per scoprirlo!)


Le versioni non parallele di trasform possono essere eseguite fuori servizio se tutti gli iteratori sono sufficientemente potenti. Nell'esempio della domanda non lo sono, quindi la specializzazione di transformdeve essere in ordine.
Caleth,

No, potrebbero non farlo, perché LegacyOutputIteratorti costringe a usarlo in ordine.
Timmmm,

Può specializzarsi diversamente per std::back_insert_iterator<std::vector<T>>e std::vector<T>::iterator. Il primo deve essere in ordine. Il secondo non ha tali restrizioni
Caleth,

Ah aspetta, capisco cosa intendi: se ti capita di passare una LegacyForwardIteratornon parallela transform, potrebbe esserci una specializzazione per ciò che lo fa fuori servizio. Buon punto.
Timmmm,

0

Credo che la trasformazione sia garantita in ordine . std::back_inserter_iteratorè un iteratore di output (il suo iterator_categorytipo membro è un alias per std::output_iterator_tag) secondo [back.insert.iterator] .

Di conseguenza, nonstd::transform ha altra opzione su come procedere alla successiva iterazione se non quella di chiamare il membro operator++sul resultparametro.

Naturalmente, questo è valido solo per sovraccarichi senza politica di esecuzione, dove std::back_inserter_iteratornon può essere utilizzato (non è un iteratore di inoltro ).


A proposito, non vorrei discutere con citazioni da cppreference. Le dichiarazioni sono spesso imprecise o semplificate. In questi casi, è meglio guardare allo standard C ++. Dove, per quanto riguarda std::transform, non vi è alcuna citazione sull'ordine delle operazioni.


"Standard C ++. Dove, per quanto riguarda std :: transform, non ci sono preventivi sull'ordine delle operazioni" Dato che l'ordine non è menzionato, non è non specificato?
HolyBlackCat

@HolyBlackCat Esplicitamente non specificato, ma imposto dall'iteratore di output. Si noti che con gli iteratori di output, una volta incrementato, è possibile non dedurre alcun valore di iteratore precedente.
Daniel Langr,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.