É válido usar std :: transform com std :: back_inserter?

20

Cppreference possui este código de exemplo para 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; });

Mas também diz:

std::transformnão garante a aplicação em ordem de unary_opou binary_op. Para aplicar uma função a uma sequência em ordem ou para aplicar uma função que modifica os elementos de uma sequência, use std::for_each.

Presumivelmente, isso permite implementações paralelas. No entanto, o terceiro parâmetro de std::transformé um LegacyOutputIteratorque possui a seguinte pós-condição para ++r:

Após esta operação, rnão é necessário incrementar e nenhuma cópia do valor anterior de rnão é mais necessária para ser desreferenciável ou incrementável.

Portanto, parece-me que a atribuição da saída deve ocorrer em ordem. Eles simplesmente significam que a aplicação de unary_oppode estar fora de ordem e armazenada em um local temporário, mas copiada na saída em ordem? Isso não soa como algo que você gostaria de fazer.

A maioria das bibliotecas C ++ ainda não implementou executores paralelos, mas a Microsoft implementou. Tenho certeza de que esse é o código relevante e acho que ele chama essa populate()função para gravar iteradores em partes da saída, o que certamente não é uma coisa válida a ser feita, porque LegacyOutputIteratorpode ser invalidada pelo aumento de cópias dele.

o que estou perdendo?

Timmmm
fonte
Um teste simples no godbolt mostra que isso é um problema. Com C ++ 20 e transformversão que decide se deve ou não usar paralelismo. O transformpara vetores grandes falha.
Croolman 11/11/19
6
@ Croolman Seu código está incorreto, pois você está inserindo novamente o sque invalida os iteradores.
Daniel Langr 11/12/19
@DanielsaysreinstateMonica Oh schnitzel, você está certo. Estava ajustando e deixando em estado inválido. Eu retiro meu comentário.
Croolman 11/12/19
Se você usar std::transformcom a política de exação, será necessário um iterador de acesso aleatório que back_inserternão pode ser cumprido. A documentação da peça citada pela IMO refere-se a esse cenário. Observe o exemplo nos usos da documentação std::back_inserter.
Marek R
@Cololman decide usar o paralelismo automaticamente?
curiousguy

Respostas:

9

1) Os requisitos do iterador de saída no padrão estão completamente quebrados. Veja LWG2035 .

2) Se você usar um iterador puramente de saída e um intervalo de fonte de entrada puramente, há pouco mais que o algoritmo possa fazer na prática; não tem escolha a não ser escrever em ordem. (No entanto, uma implementação hipotética pode optar por um caso especial para seus próprios tipos, como std::back_insert_iterator<std::vector<size_t>>; não vejo por que uma implementação desejaria fazê-lo aqui, mas é permitido fazê-lo.)

3) Nada na norma garante que transformaplique as transformações em ordem. Estamos olhando para um detalhe de implementação.

Isso std::transformrequer apenas iteradores de saída, não significa que ele não possa detectar pontos mais altos do iterador e reordenar as operações nesses casos. De fato, os algoritmos são despachados com força do iterador o tempo todo , e eles têm tratamento especial para tipos de iteradores especiais (como ponteiros ou iteradores de vetor) o tempo todo .

Quando o padrão deseja garantir uma ordem específica, sabe como dizê-la (consulte std::copy"começando firste prosseguindo para last").

TC
fonte
5

De n4385:

§25.6.4 Transformação :

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

Retorna: back_insert_iterator (x).

§23.5.2.1 Modelo de classe back_insert_iterator

using iterator_category = output_iterator_tag;

Portanto, std::back_inserternão pode ser usado com versões paralelas de std::transform. As versões que suportam iteradores de saída são lidas na origem com iteradores de entrada. Como os iteradores de entrada só podem ser pré e pós-incrementados (§23.3.5.2 iteradores de entrada) e há apenas execução sequencial ( isto é, não paralela), a ordem deve ser preservada entre eles e o iterador de saída.

Paul Evans
fonte
2
Observe que essas definições do padrão C ++ não evitam implementações para fornecer versões especiais de algoritmos selecionados para tipos adicionais de iteradores. Por exemplo, std::advancetem apenas uma definição que leva input-iteradores , mas libstdc ++ fornece versões adicionais para bidirecionais-iterators e random-access-iteradores . A versão específica é então executada com base no tipo de iterador passado .
Daniel Langr
Não acho que seu comentário esteja correto - ForwardIteratornão significa que você precise fazer as coisas em ordem. Mas você destacou o que eu perdi - para as versões paralelas que eles ForwardIteratornão usam OutputIterator.
Timmmm
11
Ah, sim, acho que estamos concordando.
Timmmm
11
Esta resposta pode se beneficiar da adição de algumas palavras para explicar o que realmente significa.
Barry
11
@ Barry Adicionado algumas palavras, todo e qualquer feedback é muito apreciado.
Paul Evans
0

Então o que eu perdi é que as versões paralelas levam LegacyForwardIterators, não LegacyOutputIterator. A LegacyForwardIterator pode ser incrementado sem invalidar cópias, portanto, é fácil usá-lo para implementar um paralelo fora de ordem std::transform.

Eu acho que as versões não paralelas de std::transform devem ser executadas em ordem. Ou cppreference está errado sobre isso, ou possivelmente o padrão apenas deixa esse requisito implícito porque não há outra maneira de implementá-lo. (Espingarda não percorrendo o padrão para descobrir!)

Timmmm
fonte
As versões não paralelas da transformação podem executar fora de ordem se todos os iteradores forem suficientemente fortes. No exemplo da pergunta, eles não são, de modo que a especialização de transformdeve estar em ordem.
Caleth
Não, eles não podem, porque LegacyOutputIteratorobriga a usá-lo em ordem.
Timmmm 12/12/19
Pode se especializar diferentemente para std::back_insert_iterator<std::vector<T>>e std::vector<T>::iterator. O primeiro deve estar em ordem. A segunda tem nenhuma restrição
Caleth
Ah, espere, eu entendo o que você quer dizer - se você passar um LegacyForwardIteratorpara o não-paralelo transform, ele pode ter uma especialização para o que faz fora de ordem. Bom ponto.
Timmmm
0

Acredito que a transformação é garantida para ser processada em ordem . std::back_inserter_iteratoré um iterador de saída (seu iterator_categorytipo de membro é um alias para std::output_iterator_tag) de acordo com [back.insert.iterator] .

Consequentemente, std::transformtem nenhuma outra opção sobre como proceder para a próxima iteração do que ao membro chamada operator++no resultparâmetro.

Obviamente, isso é válido apenas para sobrecargas sem política de execução, onde std::back_inserter_iteratornão pode ser usado (não é um iterador de encaminhamento ).


Aliás, eu não discutia com citações da cppreference. As declarações são muitas vezes imprecisas ou simplificadas. Em casos como esses, é melhor observar o padrão C ++. Onde, em relação a std::transform, não há cotação sobre a ordem das operações.

Daniel Langr
fonte
"Padrão C ++. Onde, em relação ao std :: transform, não há aspas sobre a ordem das operações" Como o pedido não é mencionado, não é especificado?
HolyBlackCat 11/12/19
@HolyBlackCat Explicitamente não especificado, mas imposto pelo iterador de saída. Observe que com os iteradores de saída, depois de incrementá-lo, você não pode desreferenciar nenhum valor anterior do iterador.
Daniel Langr 12/12/19