¿Es válido usar std :: transform con std :: back_inserter?

20

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::transformno garantiza la aplicación en orden de unary_opo binary_op. Para aplicar una función a una secuencia en orden o para aplicar una función que modifique los elementos de una secuencia, use std::for_each.

Esto es presumiblemente para permitir implementaciones paralelas. Sin embargo, el tercer parámetro de std::transformes un LegacyOutputIteratorque tiene la siguiente condición posterior para ++r:

Después de esta operación rno se requiere que sea incrementable y ya no se requiere que las copias del valor anterior rsean 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_oppuede 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 LegacyOutputIteratorpuede invalidarse incrementando copias de él.

¿Qué me estoy perdiendo?

Timmmm
fuente
Una prueba simple en godbolt muestra que esto es un problema. Con C ++ 20 y la transformversión que decide si se usa o no el paralelismo. El transformpara vectores grandes falla.
Croolman
66
@Croolman Su código es incorrecto, ya que está volviendo a insertar s, lo que invalida los iteradores.
Daniel Langr
@DanielsaysreinstateMonica Oh schnitzel tienes razón. Lo estaba ajustando y lo dejó en estado no válido. Retiro mi comentario.
Croolman
Si usa una std::transformpolítica de exacción, se requiere un iterador de acceso aleatorio que back_inserterno puede cumplir. La documentación de la parte citada por la OMI se refiere a ese escenario. Ejemplo de nota en usos de documentación std::back_inserter.
Marek R
@Croolman decide usar el paralelismo automáticamente?
curioso

Respuestas:

9

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::transformque 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 desde firsty pasando a last").

TC
fuente
5

De n4385:

§25.6.4 Transformación :

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

Devuelve: back_insert_iterator (x).

§23.5.2.1 Plantilla de clase back_insert_iterator

using iterator_category = output_iterator_tag;

Por std::back_inserterlo tanto , no se puede usar con versiones paralelas de std::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.

Paul Evans
fuente
2
Tenga en cuenta que estas definiciones del Estándar C ++ no evitan implementaciones para proporcionar versiones especiales de algoritmos que se seleccionan para tipos adicionales de iteradores. Por ejemplo, std::advancesó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 .
Daniel Langr
No creo que su comentario sea correcto. ForwardIteratorNo significa que tenga que hacer las cosas en orden. Pero ha resaltado lo que me perdí, por las versiones paralelas que ForwardIteratorno usan OutputIterator.
Timmmm
1
Ah cierto, sí, creo que estamos de acuerdo.
Timmmm
1
Esta respuesta podría beneficiarse al agregar algunas palabras para explicar lo que realmente significa.
Barry
1
@Barry Agregó algunas palabras, todos y cada uno retroalimentaron muy apreciados.
Paul Evans
0

Entonces, lo que me perdí es que las versiones paralelas toman LegacyForwardIterators, no LegacyOutputIterator. A LegacyForwardIterator puede incrementarse sin invalidar copias de él, por lo que es fácil de usar para implementar un paralelo fuera de orden std::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!)

Timmmm
fuente
Las versiones no paralelas de transform pueden ejecutarse fuera de orden si todos los iteradores son lo suficientemente fuertes. En el ejemplo de la pregunta, no lo son, por lo que la especialización de transformdebe estar en orden.
Caleth
No, no pueden, porque LegacyOutputIteratorte obliga a usarlo en orden.
Timmmm
Se puede especializar de manera diferente para std::back_insert_iterator<std::vector<T>>y std::vector<T>::iterator. El primero debe estar en orden. El segundo no tiene esa restricción
Caleth
Ah, espera, entiendo lo que quieres decir: si pasas un LegacyForwardIteratorno paralelo transform, podría tener una especialización para lo que lo hace fuera de servicio. Buen punto.
Timmmm
0

Creo que la transformación está garantizada para ser procesada en orden . std::back_inserter_iteratores un iterador de salida (su iterator_categorytipo de miembro es un alias para std::output_iterator_tag) de acuerdo con [back.insert.iterator] .

En consecuencia, nostd::transform tiene otra opción sobre cómo proceder a la siguiente iteración que llamar al miembro operator++en el resultparámetro.

Por supuesto, esto es válido solo para sobrecargas sin política de ejecución, donde std::back_inserter_iteratorno 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.

Daniel Langr
fuente
"C ++ Standard. Donde, con respecto a std :: transform, no hay una cita sobre el orden de las operaciones" Dado que el orden no se menciona, ¿no está sin especificar?
HolyBlackCat
@HolyBlackCat Sin especificar explícitamente, pero impuesto por el iterador de salida. Tenga en cuenta que con los iteradores de salida, una vez que lo incrementa, no puede desreferenciar ningún valor de iterador anterior.
Daniel Langr