¿El ciclo 'for' basado en rangos desaprueba muchos algoritmos simples?

81

Solución de algoritmo:

std::generate(numbers.begin(), numbers.end(), rand);

Solución for-loop basada en rango:

for (int& x : numbers) x = rand();

¿Por qué querría usar los std::generatebucles for más detallados que los basados ​​en rangos en C ++ 11?

fredoverflow
fuente
14
Composabilidad? Oh, no importa, los algoritmos con iteradores a menudo no se pueden componer de todos modos ... :(
R. Martinho Fernandes
2
... cuales no son begin()y end()?
the_mandrill
6
@jrok Espero que mucha gente ya tenga una rangefunción en su caja de herramientas. (ie for(auto& x : range(first, last)))
R. Martinho Fernandes
14
boost::generate(numbers, rand); // ♪
Xeo
5
@JamesBrock Hemos hablado de esto a menudo en la sala de chat de C ++ (debería estar en algún lugar de las transcripciones: P). El problema principal es que los algoritmos a menudo devuelven un iterador y toman dos iteradores.
R. Martinho Fernandes

Respuestas:

79

La primera versión

std::generate(numbers.begin(), numbers.end(), rand);

nos dice que desea generar una secuencia de valores.

En la segunda versión, el lector tendrá que averiguarlo por sí mismo.

Por lo general, ahorrar escribiendo no es óptimo, ya que la mayoría de las veces se pierde en el tiempo de lectura. La mayor parte del código se lee mucho más de lo que se escribe.

Bo Persson
fuente
13
¿Ahorras escribiendo? Oh ya entiendo. ¿Por qué tenemos el mismo término para "verificaciones de cordura en tiempo de compilación" y "presionar teclas en un teclado"? :)
fredoverflow
25
" Ahorrar escribiendo es generalmente subóptimo " Tonterías; se trata de qué biblioteca estás usando. std :: generate es largo porque debe especificar numbersdos veces sin ningún motivo. Por lo tanto: boost::range::generate(numbers, rand);. No hay ninguna razón por la que no pueda tener un código más corto y más legible en una biblioteca bien construida.
Nicol Bolas
9
Todo está en el ojo del lector. La versión de bucle es comprensible con la mayoría de los antecedentes de programación: ponga un valor rand a cada elemento de la colección. Std :: generate requiere conocer C ++ reciente, o adivinar que generar en realidad significa "modificar elementos", no "devolver valores generados".
hyde
2
Si solo desea modificar parte de un contenedor, puede std::generate(number.begin(), numbers.begin()+3, rand)hacerlo, ¿no es así? Así que supongo que especificar numberdos veces puede resultar útil a veces.
Marson Mao
7
@MarsonMao: si solo tiene dos argumentos std::generate(), puede hacerlo std::generate(slice(number.begin(), 3), rand)o incluso mejor con una sintaxis hipotética de corte de rango como la std::generate(number[0:3], rand)que elimina la repetición de numbermientras aún permite la especificación flexible de parte del rango. Hacer lo contrario a partir de un argumento de tres std::generate()es más tedioso.
Lie Ryan
42

Si el ciclo for está basado en rango o no, no hace ninguna diferencia, solo simplifica el código dentro del paréntesis. Los algoritmos son más claros porque muestran la intención .

David Rodríguez - dribeas
fuente
30

Personalmente, mi lectura inicial de:

std::generate(numbers.begin(), numbers.end(), rand);

es "estamos asignando a todo en un rango. El rango es numbers. Los valores asignados son aleatorios".

Mi lectura inicial de:

for (int& x : numbers) x = rand();

es "estamos haciendo algo con todo en un rango. El rango es numbers. Lo que hacemos es asignar un valor aleatorio".

Son bastante similares, pero no idénticos. Una razón plausible por la que podría querer provocar la primera lectura es porque creo que el hecho más importante de este código es que se asigna al rango. Así que ahí está su "por qué querría ...". Lo uso generateporque en C ++ std::generatesignifica "asignación de rango". Como lo hace por cierto std::copy, la diferencia entre los dos es desde qué estás asignando.

Sin embargo, existen factores de confusión. Los bucles for basados ​​en rango tienen una forma inherentemente más directa de expresar que el rango es numbers, que los algoritmos basados ​​en iteradores. Es por eso que la gente trabaja en bibliotecas de algoritmos basados ​​en rangos: se boost::range::generate(numbers, rand);ve mejor que la std::generateversión.

En contra de eso, int&en su bucle for basado en rango hay una arruga. ¿Qué pasa si el tipo de valor del rango no lo es int, entonces estamos haciendo algo molestamente sutil aquí que depende de que sea convertible a int&, mientras que el generatecódigo solo depende del retorno de randser asignable al elemento? Incluso si el tipo de valor es int, podría detenerme a pensar si lo es o no. Por autolo tanto , lo que pospone pensar en los tipos hasta que veo lo que se asigna - con auto &xdigo "tome una referencia al elemento de rango, sea cual sea el tipo que pueda tener". En C ++ 03, los algoritmos (porque son plantillas de funciones) eran la forma de ocultar tipos exactos, ahora son una forma.

Creo que siempre ha sido el caso de que los algoritmos más simples tienen solo un beneficio marginal sobre los bucles equivalentes. Los bucles for basados ​​en rangos mejoran los bucles (principalmente al eliminar la mayor parte del texto estándar, aunque hay un poco más que eso). Por lo tanto, los márgenes se reducen y quizás cambie de opinión en algunos casos específicos. Pero todavía hay una diferencia de estilo allí.

Steve Jessop
fuente
¿Alguna vez ha visto un tipo definido por el usuario con operator int&()? :)
fredoverflow
@FredOverflow reemplaza int&con SomeClass&y ahora tienes que preocuparte por los operadores de conversión y los constructores de un solo parámetro no marcados explicit.
TemplateRex
@FredOverflow: no lo creo. Es por eso que si alguna vez sucede, no lo esperaré y no importa cuán paranoico esté al respecto ahora, me morderá si no lo pienso entonces ;-) Un objeto proxy podría funciona sobrecargando operator int&()y operator int const &() const, pero de nuevo podría funcionar sobrecargando operator int() consty operator=(int).
Steve Jessop
1
@rhalbersma: No creo que tengas que preocuparte por los constructores, ya que una referencia no constante no se une a una referencia temporal. Son solo operadores de conversión para tipos de referencia.
Steve Jessop
23

En mi opinión, el artículo 43 de STL eficaz: "Prefiero las llamadas de algoritmos a los bucles escritos a mano". sigue siendo un buen consejo.

Normalmente escribo funciones contenedoras para deshacerme del begin()/ end()hell. Si lo hace, su ejemplo se verá así:

my_util::generate(numbers, rand);

Creo que supera el rango basado en bucle for tanto en la comunicación de la intención como en la legibilidad.


Habiendo dicho eso, debo admitir que en C ++ 98 algunas llamadas de algoritmos STL produjeron código indecible y seguir "Preferir llamadas de algoritmos a bucles escritos a mano" no parecía una buena idea. Afortunadamente, las lambdas han cambiado eso.

Considere el siguiente ejemplo de Herb Sutter: Lambdas, Lambdas Everywhere .

Tarea: busque el primer elemento en v que sea > xy < y.

Sin lambdas:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

Con lambda

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );
Ali
fuente
1
Un poco ortogonal a la pregunta. Solo la primera oración aborda la pregunta.
David Rodríguez - dribeas
@ DavidRodríguez-dribeas Sí. La segunda mitad explica por qué creo que el punto 43 sigue siendo un buen consejo.
Ali
Con Boost.Lambda, es incluso mejor que con las funciones lambda de C ++: auto i = find_if (v.begin (), v.end (), _1> x && _1 <y);
sdkljhdf hda
1
+1 para envoltorios. Haciendo lo mismo. Debería haber estado en el estándar desde el día 1 (o quizás el 2 ...)
Macke
22

En mi opinión, el ciclo manual, aunque podría reducir la verbosidad, carece de forma legible:

for (int& x : numbers) x = rand();

No usaría este ciclo para inicializar 1 el rango definido por números , porque cuando lo miro, me parece que está iterando sobre un rango de números, pero en realidad no lo hace (en esencia), es decir, en lugar de leyendo del rango, está escribiendo en el rango.

La intención es mucho más clara cuando se usa std::generate.

1. Inicializar en este contexto significa dar un valor significativo a los elementos del contenedor.

Nawaz
fuente
5
¿No es eso solo porque no estás acostumbrado a los bucles for basados ​​en rangos? Me parece bastante claro que esta declaración asigna a cada elemento del rango. Está claro que generate hace lo mismo si está familiarizado con std::generate, lo que se puede asumir de un programador de C ++ (si no están familiarizados, lo buscarán, el mismo resultado).
Steve Jessop
4
@SteveJessop: Esta respuesta no difiere de las otras dos. Requiere un poco más de esfuerzo por parte del lector y es un poco más propenso a errores (¿qué pasa si olvidas un solo &carácter?) La ventaja de los algoritmos es que muestran intención, mientras que con los bucles tienes que inferir eso. Si hay un error en la implementación del ciclo, no está claro si es un error o fue intencional.
David Rodríguez - dribeas
1
@ DavidRodríguez-dribeas: esta respuesta difiere de las otras dos, OMI significativamente. Intenta profundizar en la razón por la que el autor encuentra un fragmento de código más claro / comprensible que el otro. Los demás lo expresan sin análisis. Es por eso que encuentro este lo suficientemente interesante como para responderlo :-)
Steve Jessop
1
@SteveJessop: Tienes que mirar el cuerpo del bucle para llegar a la conclusión de que en realidad estás generando números, pero en caso de que std::generate, con solo mirar, se pueda decir que esta función está generando algo ; lo que ese algo es respondido por el tercer argumento de la función. Creo que esto es mucho mejor.
Nawaz
1
@SteveJessop: Significa que perteneces a la minoría. Escribiría un código que sea más claro para la mayoría: P. Solo el último: no dije en ninguna parte que otros leerán el bucle de la misma manera que yo. Dije (más bien quise decir ) que esta es una forma de leer el bucle que es engañoso para mí, y debido a que el cuerpo del bucle está allí, diferentes programadores lo leerán de manera diferente para averiguar qué está sucediendo allí; podrían oponerse al uso de dicho bucle, por diferentes razones, todas las cuales podrían ser correctas según su percepción.
Nawaz
9

Hay algunas cosas que no puede hacer (simplemente) con bucles basados ​​en rangos que los algoritmos que toman iteradores como entrada pueden. Por ejemplo con std::generate:

Llene el contenedor hasta limit(excluido, limites un iterador válido activado numbers) con variables de una distribución y el resto con variables de otra distribución.

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

Los algoritmos basados ​​en iteradores le brindan un mejor control sobre el rango en el que está operando.

Khaur
fuente
8
Si bien el motivo de legibilidad es ENORME para preferir algoritmos, esta es la única respuesta que muestra que el bucle for basado en rango es solo un subconjunto de lo que son los algoritmos y, por lo tanto, no puede desaprobar nada ...
K-Ballo
6

Para el caso particular de std::generate, estoy de acuerdo con las respuestas anteriores sobre el problema de legibilidad / intención. std :: generate me parece una versión más clara. Pero admito que esto es en cierto modo una cuestión de gustos.

Dicho esto, tengo otra razón para no desechar el algoritmo std ::: hay ciertos algoritmos que están especializados para algunos tipos de datos.

El ejemplo más simple sería std::fill. La versión general se implementa como un bucle for sobre el rango proporcionado, y esta versión se utilizará al crear instancias de la plantilla. Pero no siempre. Por ejemplo, si le proporciona un rango que es un std::vector<int>- a menudo llamará memsetbajo el capó, produciendo un código mucho más rápido y mejor.

Así que estoy tratando de jugar una carta de eficiencia aquí.

Su bucle escrito a mano puede ser tan rápido como una versión del algoritmo std ::, pero difícilmente puede ser más rápido. Y más que eso, el algoritmo std :: puede estar especializado para contenedores y tipos particulares y se hace bajo la interfaz limpia de STL.

Sergei Nosov
fuente
3

Mi respuesta sería tal vez y no. Si estamos hablando de C ++ 11, entonces tal vez (más como no). Por ejemplo, std::for_eaches realmente molesto usarlo incluso con lambdas:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
    // do stuff with x
});

Pero usar basado en rango para es mucho mejor:

for (auto& x : c)
{
    // do stuff with x
}

Por otro lado, si estamos hablando de C ++ 1y, entonces diría que no, los algoritmos no quedarán obsoletos por rango basado en. En el comité estándar de C ++ hay un grupo de estudio que está trabajando en una propuesta para agregar rangos a C ++, y también se está trabajando en lambdas polimórficas. Los rangos eliminarían la necesidad de usar un par de iteradores y lambda polimórfica le permitiría no especificar el tipo de argumento exacto de lambda. Esto significa que std::for_eachpodría usarse así (no lo tome como un hecho difícil, así es como se ven los sueños hoy):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});
sdkljhdf hda
fuente
Entonces, en el último caso, ¿la ventaja del algoritmo sería que al escribir []con la lambda especificas captura cero? Es decir, en comparación con simplemente escribir un cuerpo de bucle, ha aislado un fragmento de código del contexto de búsqueda de variables en el que aparece léxicamente. El aislamiento suele ser útil para el lector, menos en qué pensar mientras lee.
Steve Jessop
1
La captura no es el punto. El punto es que con lambda polimórfica no necesitará especificar explícitamente cuál es el tipo de x.
sdkljhdf hda
1
En ese caso, me parece que en este hipotético C ++ 1y, for_eachtodavía no tiene sentido incluso cuando se usa con una lambda. foreach + capturar lambda es actualmente una forma detallada de escribir un bucle for basado en rangos, y se vuelve un poco menos detallado pero aún más que el bucle. No es que crea que debas defender for_each, por supuesto, pero incluso antes de ver tu respuesta, estaba pensando que si el interrogador quería vencer a los algoritmos, podría haber elegido for_eachcomo el más suave de todos los objetivos posibles ;-)
Steve Jessop
No va a defender for_each, pero tiene una pequeña ventaja sobre el rango basado en: puede hacer que sea paralelo más fácilmente simplemente prefijándolo con paralelo_ para convertirlo en parallel_for_each(si usa PPL, y asumiendo que es seguro para subprocesos hacerlo) . :-D
sdkljhdf hda
@lego Su "pequeña" ventaja es de hecho una "gran" ventaja si la generaliza hasta el hecho de que la std::algorithmimplementación de s está oculta detrás de su interfaz y puede ser arbitrariamente compleja (u optimizada arbitrariamente).
Christian Rau
1

Una cosa que debe notarse es que un algoritmo expresa lo que se hace, no cómo.

El ciclo basado en rango incluye la forma en que se hacen las cosas: comience con el primero, aplique y continúe con el siguiente elemento hasta el final. Incluso un algoritmo simple podría hacer las cosas de manera diferente (al menos algunas sobrecargas para contenedores específicos, ni siquiera pensar en el vector horrible), y al menos la forma en que se hace no es el negocio del escritor.

Para mí, esa es gran parte de la diferencia, encapsula todo lo que puedas, y eso justifica la oración cuando puedas, usa algoritmos.

Cedric
fuente
1

El bucle for basado en rango es solo eso. Hasta que, por supuesto, se cambie el estándar.

El algoritmo es una función. Una función que impone algunos requisitos a sus parámetros. Los requisitos están redactados en un estándar para permitir, por ejemplo, una implementación que aproveche todos los subprocesos de ejecución disponibles y lo acelerará automáticamente.

Tomek
fuente