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::transform
não garante a aplicação em ordem deunary_op
oubinary_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, usestd::for_each
.
Presumivelmente, isso permite implementações paralelas. No entanto, o terceiro parâmetro de std::transform
é um LegacyOutputIterator
que possui a seguinte pós-condição para ++r
:
Após esta operação,
r
não é necessário incrementar e nenhuma cópia do valor anterior der
nã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_op
pode 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 LegacyOutputIterator
pode ser invalidada pelo aumento de cópias dele.
o que estou perdendo?
fonte
transform
versão que decide se deve ou não usar paralelismo. Otransform
para vetores grandes falha.s
que invalida os iteradores.std::transform
com a política de exação, será necessário um iterador de acesso aleatório queback_inserter
nã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çãostd::back_inserter
.Respostas:
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
transform
aplique as transformações em ordem. Estamos olhando para um detalhe de implementação.Isso
std::transform
requer 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çandofirst
e prosseguindo paralast
").fonte
De
n4385
:§25.6.4 Transformação :
§23.5.2.1.2 back_inserter
§23.5.2.1 Modelo de classe back_insert_iterator
Portanto,
std::back_inserter
não pode ser usado com versões paralelas destd::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.fonte
std::advance
tem 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 .ForwardIterator
não significa que você precise fazer as coisas em ordem. Mas você destacou o que eu perdi - para as versões paralelas que elesForwardIterator
não usamOutputIterator
.Então o que eu perdi é que as versões paralelas levam
LegacyForwardIterator
s, nãoLegacyOutputIterator
. ALegacyForwardIterator
pode ser incrementado sem invalidar cópias, portanto, é fácil usá-lo para implementar um paralelo fora de ordemstd::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!)fonte
transform
deve estar em ordem.LegacyOutputIterator
obriga a usá-lo em ordem.std::back_insert_iterator<std::vector<T>>
estd::vector<T>::iterator
. O primeiro deve estar em ordem. A segunda tem nenhuma restriçãoLegacyForwardIterator
para o não-paralelotransform
, ele pode ter uma especialização para o que faz fora de ordem. Bom ponto.Acredito que a transformação é garantida para ser processada em ordem .
std::back_inserter_iterator
é um iterador de saída (seuiterator_category
tipo de membro é um alias parastd::output_iterator_tag
) de acordo com [back.insert.iterator] .Consequentemente,
std::transform
tem nenhuma outra opção sobre como proceder para a próxima iteração do que ao membro chamadaoperator++
noresult
parâmetro.Obviamente, isso é válido apenas para sobrecargas sem política de execução, onde
std::back_inserter_iterator
nã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.fonte