¿Prefieres algoritmos a bucles escritos a mano?


10

¿Cuál de los siguientes le parece más legible? El bucle escrito a mano:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

O la invocación del algoritmo:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Me pregunto si std::for_eachrealmente vale la pena, dado que un ejemplo tan simple ya requiere mucho código.

¿Qué piensas sobre este asunto?


2
Para C ++, la respuesta es bastante obvia. Pero en otros idiomas, ambos son casi iguales (por ejemplo, Python: map(bar.process, vec)aunque se desaconseja el mapa de efectos secundarios y se recomiendan las comprensiones de lista / expresiones generadoras sobre el mapa).

55
Y luego también está BOOST_FOREACH...
Benjamin Bannier

Entonces, ¿el artículo en el STL efectivo no te convenció? stackoverflow.com/questions/135129/…
Trabajo

Respuestas:


18

Hay una razón por la que se introdujeron las lambdas, y es porque incluso el Comité Estándar reconoce que la segunda forma apesta. Use la primera forma, hasta que obtenga soporte para C ++ 0x y lambda.


2
El mismo razonamiento puede usarse para justificar la segunda forma: el Comité Estándar reconoció que era tan superior que introdujeron lambdas para hacerlo aún mejor. :-p (+1 sin embargo)
Konrad Rudolph

No creo que lo segundo sea tan malo. Pero puede ser mejor. Por ejemplo, deliberadamente hizo que el objeto Bar sea más difícil de usar al no agregar operator (). Al usar esto, simplifica enormemente el punto de llamada en el bucle y hace que el código sea ordenado.
Martin York

Nota: se agregó soporte lambda debido a dos quejas importantes. 1) La placa estándar necesaria para pasar los parámetros a los functors. 2) No tener el código en el punto de llamada. Su código no satisface ninguno de estos, ya que solo está llamando a un método. Entonces, 1) no hay código adicional para pasar parámetros 2) El código ya no está en el punto de llamada, por lo que lambda no mejorará el mantenimiento del código. Entonces sí lambda son una gran adición. Este no es un buen argumento para ellos.
Martin York

9

Utilice siempre la variante que describe mejor lo que pretende hacer. Es decir

Para cada elemento xen vec, hacer bar.process(x).

Ahora, examinemos los ejemplos:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

También tenemos una for_each, sí, . Tenemos el [begin; end)rango en el que queremos operar.

En principio, el algoritmo era mucho más explícito y, por lo tanto, preferible a cualquier implementación escrita a mano. Pero entonces ... Binders? Memfun? Básicamente C ++ interna de cómo obtener una función miembro? ¡Para mi tarea, no me importan ! Tampoco quiero sufrir esta sintaxis verbosa y espeluznante.

Ahora la otra posibilidad:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Por supuesto, este es un patrón común para reconocer, pero ... creando iteradores, bucles, incrementos, desreferenciación. Estas también son cosas que no me importan para poder hacer mi tarea.

Es cierto que se ve mucho mejor que la primera solución (al menos, el cuerpo del bucle es flexible y bastante explícito), pero aún así, no es realmente tan bueno. Usaremos este si no tuviéramos una mejor posibilidad, pero tal vez tengamos ...

¿Una mejor manera?

Ahora de vuelta a for_each. ¿No sería genial decir literalmente for_each y ser flexible en la operación que también se debe hacer? Afortunadamente, desde C ++ 0x lambdas, somos

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Ahora que hemos encontrado una solución abstracta y genérica para muchas situaciones relacionadas, vale la pena señalar que en este caso particular, hay un favorito absoluto # 1 :

foreach(const Foo& x, vec) bar.process(x);

Realmente no puede ser mucho más claro que eso. Afortunadamente, C ++ 0x get tiene una sintaxis similar incorporada .


2
Dicho "sintaxis similar" es for (const Foo& x : vec) bar.process(x);, o usa const auto&si lo desea.
Jon Purdy

const auto&¿es posible? No lo sabía, ¡gran información!
Dario

8

¿Porque esto es tan ilegible?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}

44
Lo sentimos, pero por alguna razón dice "censurado" donde creo que debería estar su código.
Mateen Ulhaq

66
Su código da una advertencia del compilador, porque comparar un intcon un size_tes peligroso. Además, la indexación no funciona con todos los contenedores, por ejemplo std::set.
fredoverflow

2
Este código solo funcionará para contenedores con un operador de indexación, de los cuales hay pocos. Vincula el algoritmo de forma poco práctica: ¿qué pasaría si un mapa <> se adaptara mejor? El original for loop y for_each no se vería afectado.
JBRWilkinson

2
¿Y cuántas veces diseñas el código para un vector y luego decides cambiarlo por un mapa? Como si diseñar para eso fuera la prioridad de los idiomas.
Martin Beckett

2
Se desaconseja iterar sobre colecciones por índice porque algunas colecciones tienen un bajo rendimiento en la indexación, mientras que todas las colecciones se comportan bien cuando se usa un iterador.
slaphappy

3

en general, la primera forma es legible por cualquiera que sepa lo que es un bucle for, sin importar el fondo que tengan.

También en general, el segundo no es tan fácil de leer: es bastante fácil imaginar qué está haciendo for_each, pero si nunca lo has visto std::bind1st(std::mem_fun_ref, puedo imaginar que es difícil de entender.


2

En realidad, incluso con C ++ 0x, no estoy seguro de for_eachque reciba mucho amor.

for(Foo& foo: vec) { bar.process(foo); }

Es mucho más legible.

Lo único que no me gusta de los algoritmos (en C ++) es que razonan en iteradores, lo que hace declaraciones muy detalladas.


2

Si hubiera escrito bar como functor, entonces sería mucho más simple:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Aquí el código es bastante legible.


2
Es bastante legible, aparte del hecho de que acaba de escribir un functor.
Winston Ewert

Vea aquí por qué creo que este código es peor.
sbi

1

Prefiero el último porque está limpio y ordenado. En realidad, es parte de muchos otros lenguajes, pero en C ++, es parte de la biblioteca. Realmente no importa.


0

Ahora, este problema en realidad no es específico de C ++, diría.

La cuestión es que pasar algún functor a otra función más no pretende reemplazar los bucles .
Se supone que simplifica ciertos patrones, que se vuelven muy naturales con la programación funcional, como visitante y observador , solo por nombrar dos, que vienen a mi mente de inmediato.
En lenguajes que carecen de funciones de primer orden (Java probablemente es el mejor ejemplo), tales enfoques siempre requieren implementar alguna interfaz dada, que es bastante verbosa y redundante.

Un uso común que veo mucho en otros idiomas sería:

someCollection.forEach(someUnaryFunctor);

La ventaja de esto es que no necesita saber cómo someCollectionse implementa realmente o qué someUnaryFunctorhace. Todo lo que necesita saber es que su forEachmétodo iterará todos los elementos de la colección, pasándolos a la función dada.

Personalmente, si está en la posición de tener toda la información sobre la estructura de datos sobre la que desea iterar y toda la información sobre lo que desea hacer en cada paso de iteración, entonces un enfoque funcional es complicar las cosas, especialmente en un lenguaje, donde esto es aparentemente bastante tedioso.

Además, debe tener en cuenta que el enfoque funcional es más lento, porque tiene muchas llamadas, que tienen un costo determinado, que no tiene en un bucle for.


1
"Además, debe tener en cuenta que el enfoque funcional es más lento, porque tiene muchas llamadas, que tienen un costo determinado, que no tiene en un bucle for". ? Esta declaración no es compatible. El enfoque funcional no es necesariamente más lento, especialmente porque puede haber optimizaciones debajo del capó. E incluso si comprende completamente su ciclo, probablemente se verá más limpio en su forma más abstracta.
Andres F.

@Andres F: ¿Entonces esto se ve más limpio? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));Y es más lento en tiempo de ejecución o tiempo de compilación (si tienes suerte). No veo por qué reemplazar las estructuras de control de flujo con llamadas a funciones mejora el código. Sobre cualquier alternativa presentada aquí es más legible.
back2dos

0

Creo que el problema aquí es que for_eachno es realmente un algoritmo, es solo una forma diferente (y generalmente inferior) de escribir un bucle escrito a mano. desde esta perspectiva, debería ser el algoritmo estándar menos utilizado, y sí, probablemente también debería usar un bucle for. sin embargo, el consejo en el título sigue en pie, ya que hay varios algoritmos estándar diseñados para usos más específicos.

Donde hay un algoritmo que hace más estrictamente lo que quiere, entonces sí, debería preferir algoritmos a loops escritos a mano. ejemplos obvios aquí son ordenar con sorto stable_sortbuscar con lower_bound, upper_boundo equal_range, pero la mayoría de ellos tienen alguna situación en la que es preferible usarlos en un bucle codificado a mano.


0

Para este pequeño fragmento de código, ambos son igualmente legibles para mí. En general, encuentro bucles manuales propensos a errores y prefiero usar algoritmos, pero realmente depende de una situación concreta.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.