Cppreference tiene este código de ejemplo 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; });
Pero también dice:
std::transform
no garantiza la aplicación en orden deunary_op
obinary_op
. Para aplicar una función a una secuencia en orden o para aplicar una función que modifique los elementos de una secuencia, usestd::for_each
.
Esto es presumiblemente para permitir implementaciones paralelas. Sin embargo, el tercer parámetro de std::transform
es un LegacyOutputIterator
que tiene la siguiente condición posterior para ++r
:
Después de esta operación
r
no se requiere que sea incrementable y ya no se requiere que las copias del valor anteriorr
sean desreferenciables o incrementables
Entonces me parece que la asignación de la salida debe ocurrir en orden. ¿Simplemente significan que la aplicación de unary_op
puede estar fuera de servicio y almacenada en una ubicación temporal, pero copiada a la salida en orden? Eso no suena como algo que alguna vez quieras hacer.
La mayoría de las bibliotecas de C ++ aún no han implementado ejecutores paralelos, pero Microsoft sí. Estoy bastante seguro de que este es el código relevante, y creo que llama a esta populate()
función para grabar iteradores en fragmentos de la salida, lo que seguramente no es algo válido porque LegacyOutputIterator
puede invalidarse incrementando copias de él.
¿Qué me estoy perdiendo?
fuente
transform
versión que decide si se usa o no el paralelismo. Eltransform
para vectores grandes falla.s
, lo que invalida los iteradores.std::transform
política de exacción, se requiere un iterador de acceso aleatorio queback_inserter
no puede cumplir. La documentación de la parte citada por la OMI se refiere a ese escenario. Ejemplo de nota en usos de documentaciónstd::back_inserter
.Respuestas:
1) Los requisitos del iterador de salida en el estándar están completamente rotos. Ver LWG2035 .
2) Si usa un iterador puramente de salida y un rango de fuente de entrada pura, entonces hay poco más que el algoritmo puede hacer en la práctica; no tiene más remedio que escribir en orden. (Sin embargo, una implementación hipotética puede optar por casos especiales de sus propios tipos, como
std::back_insert_iterator<std::vector<size_t>>
; no veo por qué una implementación querría hacerlo aquí, pero está permitido hacerlo).3) Nada en la norma garantiza que
transform
aplica las transformaciones en orden. Estamos viendo un detalle de implementación.El hecho de
std::transform
que solo requiera iteradores de salida no significa que no pueda detectar fortalezas de iterador más altas y reordenar las operaciones en tales casos. De hecho, los algoritmos se distribuyen en la fuerza del iterador todo el tiempo , y tienen un manejo especial para los tipos de iteradores especiales (como punteros o iteradores de vectores) todo el tiempo .Cuando el estándar quiere garantizar un pedido en particular, sabe cómo decirlo (vea
std::copy
"comenzando desdefirst
y pasando alast
").fuente
De
n4385
:§25.6.4 Transformación :
§23.5.2.1.2 back_inserter
§23.5.2.1 Plantilla de clase back_insert_iterator
Por
std::back_inserter
lo tanto , no se puede usar con versiones paralelas destd::transform
. Las versiones que admiten iteradores de salida leen desde su fuente con iteradores de entrada. Dado que los iteradores de entrada solo pueden incrementarse antes y después (§23.3.5.2 iteradores de entrada) y solo hay ejecución secuencial ( es decir, no paralela), el orden debe preservarse entre ellos y el iterador de salida.fuente
std::advance
sólo tiene una definición que toma de entrada-iteradores , pero libstdc ++ proporciona versiones adicionales para bidireccionales-iteradores y de acceso aleatorio iteradores . La versión particular se ejecuta en función del tipo de iterador pasado .ForwardIterator
No significa que tenga que hacer las cosas en orden. Pero ha resaltado lo que me perdí, por las versiones paralelas queForwardIterator
no usanOutputIterator
.Entonces, lo que me perdí es que las versiones paralelas toman
LegacyForwardIterator
s, noLegacyOutputIterator
. ALegacyForwardIterator
puede incrementarse sin invalidar copias de él, por lo que es fácil de usar para implementar un paralelo fuera de ordenstd::transform
.Creo que las versiones no paralelas de
std::transform
deben ejecutarse en orden. O bien cppreference está equivocado al respecto, o posiblemente el estándar simplemente deja implícito este requisito porque no hay otra forma de implementarlo. (¡La escopeta no vadea el estándar para descubrirlo!)fuente
transform
debe estar en orden.LegacyOutputIterator
te obliga a usarlo en orden.std::back_insert_iterator<std::vector<T>>
ystd::vector<T>::iterator
. El primero debe estar en orden. El segundo no tiene esa restricciónLegacyForwardIterator
no paralelotransform
, podría tener una especialización para lo que lo hace fuera de servicio. Buen punto.Creo que la transformación está garantizada para ser procesada en orden .
std::back_inserter_iterator
es un iterador de salida (suiterator_category
tipo de miembro es un alias parastd::output_iterator_tag
) de acuerdo con [back.insert.iterator] .En consecuencia, no
std::transform
tiene otra opción sobre cómo proceder a la siguiente iteración que llamar al miembrooperator++
en elresult
parámetro.Por supuesto, esto es válido solo para sobrecargas sin política de ejecución, donde
std::back_inserter_iterator
no se puede usar (no es un iterador de reenvío ).Por cierto, no discutiría con citas de cppreference. Las declaraciones allí a menudo son imprecisas o simplificadas. En casos como estos, es mejor mirar el Estándar C ++. Donde, con respecto a
std::transform
, no hay una cita sobre el orden de las operaciones.fuente