¿Puedo devolver una canalización temporal a una operación de rango?

9

Supongamos que tengo una generate_my_rangeclase que modela a range(en particular, es regular). Entonces es el siguiente código correcto:

auto generate_my_range(int some_param) {    
  auto my_transform_op = [](const auto& x){ return do_sth(x); };
  return my_custom_rng_gen(some_param) | ranges::views::transform(my_transform_op);
}
auto cells = generate_my_range(10) | ranges::to<std::vector>;

¿Es my_custom_rng_gen(some_param)tomado por valor por el (primer) operador de tubería, o tengo una referencia colgante una vez que salgo del generate_my_rangealcance?

¿Sería lo mismo con la llamada funcional ranges::views::transform(my_custom_rng_gen(some_param),my_transform_op)?

¿Sería correcto si usara una referencia lvalue? p.ej:

auto generate_my_range(int some_param) {
  auto my_transform_op = [](const auto& x){ return do_sth(x); };
  auto tmp_ref = my_custom_rng_gen(some_param);
  return tmp_ref | ranges::views::transform(my_transform_op);
}

Si se toman rangos por valores para estas operaciones, ¿qué debo hacer si paso una referencia de valor a un contenedor? ¿Debo usar un ranges::views::all(my_container)patrón?

Bérenger
fuente
¿My_custom_rng_gen (some_param) ya está acotado? ¿Te refieres a algo como godbolt.org/z/aTF8RN sin la toma (5)?
Porsche9II
@ Porsche9II Sí, este es un rango acotado. Digamos que es un contenedor
Bérenger

Respuestas:

4

En la biblioteca de rangos hay dos tipos de operaciones:

  • vistas que son perezosas y requieren que exista el contenedor subyacente.
  • acciones que están ansiosas y producen nuevos contenedores como resultado (o modifican los existentes)

Las vistas son ligeras. Los pasa por valor y requiere que los contenedores subyacentes permanezcan válidos y sin cambios.

De la documentación de la gama v3

Una vista es un contenedor ligero que presenta una vista de una secuencia subyacente de elementos de alguna manera personalizada sin mutarla o copiarla. Las vistas son baratas de crear y copiar y tienen una semántica de referencia no propietaria.

y:

Cualquier operación en el rango subyacente que invalide sus iteradores o centinelas también invalidará cualquier vista que se refiera a cualquier parte de ese rango.

La destrucción del contenedor subyacente obviamente invalida todos los iteradores.

En su código, está utilizando vistas específicamente : usted usa ranges::views::transform. La tubería es simplemente un azúcar sintáctico para que sea fácil escribir de la forma en que está. Debería mirar lo último en la tubería para ver lo que produce, en su caso, es una vista.

Si no hubiera operador de tubería, probablemente se vería así:

ranges::views::transform(my_custom_rng_gen(some_param), my_transform_op)

si hubiera múltiples transformaciones conectadas de esa manera, puede ver lo feo que se pondría.

Por lo tanto, si my_custom_rng_genproduce algún tipo de contenedor, que transforma y luego devuelve, ese contenedor se destruye y tiene referencias colgantes desde su vista. Si my_custom_rng_genes otra vista de un contenedor que vive fuera de estos ámbitos, todo está bien.

Sin embargo, el compilador debería poder reconocer que está aplicando una vista en un contenedor temporal y recibir un error de compilación.

Si desea que su función devuelva un rango como contenedor, debe "materializar" explícitamente el resultado. Para eso, use el ranges::tooperador dentro de la función.


Actualización: para ser más explícito con respecto a su comentario "¿dónde dice la documentación que componer el rango / tubería toma y almacena una vista?"

Pipe es simplemente un azúcar sintáctico para conectar cosas en una expresión fácil de leer. Dependiendo de cómo se use, puede o no devolver una vista. Depende del argumento del lado derecho. En tu caso es:

`<some range> | ranges::views::transform(...)`

Entonces la expresión devuelve lo que sea que views::transformregrese.

Ahora, leyendo la documentación de la transformación:

A continuación se muestra una lista de los combinadores de rango diferido, o vistas, que proporciona Range-v3, y un resumen sobre cómo se pretende utilizar cada uno.

[...]

views::transform

Dado un rango fuente y una función unaria, devuelve un nuevo rango donde cada elemento resultante es el resultado de aplicar la función unaria a un elemento fuente.

Por lo tanto, devuelve un rango, pero dado que es un operador perezoso, ese rango que devuelve es una vista, con toda su semántica.

CygnusX1
fuente
Okay. Lo que es un poco misterioso para mí es cómo funciona cuando paso un contenedor a la tubería (es decir, el objeto de rango creado por la composición). Necesita almacenar una vista del contenedor de alguna manera. ¿Se hace con ranges::views::all(my_container)? ¿Y qué pasa si se pasa una vista a la tubería? ¿Reconoce que se le pasa un contenedor o una vista? ¿Necesita hacerlo? ¿Cómo?
Bérenger
"El compilador debería ser capaz de reconocer que estás aplicando una vista en un contenedor temporal y golpearte con un error de compilación". Eso es lo que yo también pensé: si hago algo estúpido, significa un contrato sobre el tipo (ser una izquierda valor) no se cumple. Cosas como esa se hacen mediante range-v3. Pero en este caso no hay absolutamente ningún problema. Compila Y corre. Por lo tanto, puede haber un comportamiento indefinido, pero no aparece.
Bérenger
Para estar seguro si su código se ejecuta correctamente por accidente o si todo está bien, necesitaría ver el contenido de my_custom_rng_gen. Cómo exactamente la tubería e transforminteractuar bajo el capó no es importante. La expresión completa toma un rango como argumento (un contenedor o una vista para algún contenedor) y devuelve una vista diferente a ese contenedor. El valor de retorno nunca será el propietario del contenedor, porque es una vista.
CygnusX1
1

Tomado de la documentación degamas-v3 :

Las vistas tienen una semántica de referencia no propietaria.

y

Tener un solo objeto de rango permite tuberías de operaciones. En una tubería, un rango se adapta perezosamente o se muta ansiosamente de alguna manera, con el resultado inmediatamente disponible para una mayor adaptación o mutación. La adaptación perezosa se maneja mediante vistas, y la mutación ansiosa se maneja mediante acciones.

// taken directly from the the ranges documentation
std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){ return i % 2 == 1; })
              | views::transform([](int i){ return std::to_string(i); });
// rng == {"2","4","6","8","10"};

En el código anterior, rng simplemente almacena una referencia a los datos subyacentes y las funciones de filtro y transformación. No se realiza ningún trabajo hasta que se repite rng.

Como usted dijo que el rango temporal puede considerarse como un contenedor, su función devuelve una referencia colgante.

En otras palabras, debe asegurarse de que el rango subyacente sobreviva a la vista, o está en problemas.

Rumburak
fuente
Sí, las vistas no son propias, pero ¿dónde dice la documentación que componer el rango / tubería toma y almacena una vista? Sería posible (y creo que es algo bueno) tener la siguiente política: almacenar por valor si el rango viene dado por una referencia de valor.
Bérenger
1
@ Bérenger Agregué un poco más de la documentación de rangos. Pero el punto realmente es: una vista no es propietaria . No le importa si le das un valor.
Rumburak