Con casi todo el código que escribo, a menudo me enfrento con problemas de reducción de conjuntos en colecciones que, en última instancia, terminan con condiciones ingenuas "si" dentro de ellas. He aquí un ejemplo sencillo:
for(int i=0; i<myCollection.size(); i++)
{
if (myCollection[i] == SOMETHING)
{
DoStuff();
}
}
Con lenguajes funcionales, puedo resolver el problema reduciendo la colección a otra colección (fácilmente) y luego realizar todas las operaciones en mi conjunto reducido. En pseudocódigo:
newCollection <- myCollection where <x=true
map DoStuff newCollection
Y en otras variantes de C, como C #, podría reducir con una cláusula where como
foreach (var x in myCollection.Where(c=> c == SOMETHING))
{
DoStuff();
}
O mejor (al menos a mis ojos)
myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));
Es cierto que estoy haciendo mucha mezcla de paradigmas y estilo subjetivo / basado en opiniones, pero no puedo evitar sentir que me falta algo realmente fundamental que podría permitirme usar esta técnica preferida con C ++. ¿Alguien podría iluminarme?
std::copy_if
, pero las selecciones no son perezosasif
interiorfor
que mencionas no solo es funcionalmente equivalente a los otros ejemplos, sino que probablemente también sería más rápido en muchos casos. También para alguien que dice que le gusta el estilo funcional, lo que está promocionando parece ir en contra del concepto de pureza muy querido de la programación funcional, ya queDoStuff
claramente tiene efectos secundarios.Respuestas:
En mi humilde opinión, es más sencillo y más legible usar un bucle for con un if dentro. Sin embargo, si esto le resulta molesto, puede utilizar uno
for_each_if
como el siguiente:Caso de uso:
Demo en vivo
fuente
find_if
yfind
si funcionan en rangos o pares de iteradores. (Hay algunas excepciones, comofor_each
yfor_each_n
). La forma de evitar escribir nuevos algos para cada estornudo es usar diferentes operaciones con los algos existentes, por ejemplo, en lugar defor_each_if
incrustar la condición en el invocable pasado afor_each
, por ejemplofor_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Boost proporciona rangos que se pueden usar con rango basado en. Los rangos tienen la ventaja de que no copian la estructura de datos subyacente, simplemente proporcionan una 'vista' (es decir
begin()
,end()
para el rango yoperator++()
,operator==()
para el iterador). Esto puede ser de su interés: http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.htmlfuente
is_even
=>condition
,input
=>myCollection
etc.filtered()
para usted; dicho esto, es mejor usar una lib compatible que un código ad-hoc.En lugar de crear un nuevo algoritmo, como lo hace la respuesta aceptada, puede usar uno existente con una función que aplique la condición:
O si realmente desea un nuevo algoritmo, al menos reutilícelo
for_each
allí en lugar de duplicar la lógica de iteración:fuente
std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });
es totalmente más simple quefor (Iter x = first; x != last; x++) if (p(x)) op(x);}
?std::for_each(std::execution::par, first, last, ...);
¿Qué tan fácil es agregar esas cosas a un bucle escrito a mano?La idea de evitar
constructos como antipatrón es demasiado amplio.
Está completamente bien procesar varios elementos que coincidan con una determinada expresión desde el interior de un bucle, y el código no puede ser mucho más claro que eso. Si el procesamiento crece demasiado para caber en la pantalla, esa es una buena razón para usar una subrutina, pero aún así, es mejor colocar el condicional dentro del ciclo, es decir
es mucho mejor que
Se convierte en un antipatrón cuando solo un elemento coincidirá, porque entonces sería más claro buscar primero el elemento y realizar el procesamiento fuera del bucle.
es un ejemplo extremo y obvio de esto. Más sutil, y por lo tanto más común, es un patrón de fábrica como
Esto es difícil de leer, porque no es obvio que el código del cuerpo se ejecutará una sola vez. En este caso, sería mejor separar la búsqueda:
Todavía hay un
if
dentro de afor
, pero del contexto queda claro lo que hace, no hay necesidad de cambiar este código a menos que la búsqueda cambie (por ejemplo, a amap
), y está inmediatamente claro quecreate_object()
se llama solo una vez, porque es no dentro de un bucle.fuente
for( range ){ if( condition ){ action } }
estilo-hace que sea fácil leer las cosas de una en una y solo usa el conocimiento de las construcciones básicas del lenguaje.for(...) if(...) { ... }
menudo es la mejor opción (por eso califiqué la recomendación de dividir la acción en una subrutina).for(…)if(…)…
si fuera el único lugar donde se produjo la búsqueda.Aquí hay una función rápida relativamente mínima
filter
.Requiere un predicado. Devuelve un objeto de función que toma un iterable.
Devuelve un iterable que se puede utilizar en un
for(:)
bucle.Tomé atajos. Una biblioteca real debe hacer iteradores reales, no los
for(:)
pseudofascadas de calificación que hice.En el punto de uso, se ve así:
que es bastante bonito y se imprime
Ejemplo vivo .
Hay una adición propuesta a C ++ llamada Rangesv3 que hace este tipo de cosas y más.
boost
también tiene rangos de filtro / iteradores disponibles. boost también tiene ayudantes que hacen que escribir lo anterior sea mucho más corto.fuente
Un estilo que se acostumbra lo suficiente como para mencionarlo, pero que aún no se ha mencionado, es:
Ventajas:
DoStuff();
cuando aumenta la complejidad de la condición. Lógicamente,DoStuff();
debería estar en el nivel superior de lafor
ciclo, y lo es.SOMETHING
s de la colección, sin requerir que el lector verifique que no hay nada después del cierre}
de la colección.if
bloque.Desventajas:
continue
, Al igual que otras sentencias de control de flujo, se utilice incorrectamente de manera que conduzcan al código duro de seguir el tanto que algunas personas se oponen a cualquier uso de ellos: hay un estilo válido de codificación que algunos seguimiento que evitacontinue
, que evitabreak
que no sea en aswitch
, que evitareturn
otro que al final de una función.fuente
for
bucle que se extiende a muchas líneas, un "si no, continúa" de dos líneas es mucho más claro, lógico y legible. Decir inmediatamente "omita esto si" después de que lafor
declaración se lea bien y, como dijiste, no sangra los aspectos funcionales restantes del ciclo.continue
Sin embargo, si está más abajo, se sacrifica algo de claridad (es decir, si siempre se realizará alguna operación antes de laif
declaración).for
Para mí, se parece mucho a una comprensión específica de C ++ . ¿Para ti?fuente
auto const
no tiene nada que ver con el orden de iteración. Si busca basado enfor
rangos, verá que básicamente hace un bucle estándar debegin()
aend()
con desreferenciación implícita. No hay forma de que pueda romper las garantías de pedido (si las hay) del contenedor que se está iterando; se habría reído de la faz de la Tierrastd::future
s,std::function
s, incluso esos cierres anónimos están muy bien en C ++ en la sintaxis; cada idioma tiene su propio lenguaje y, al incorporar nuevas características, intenta que imiten la antigua sintaxis conocida.Si DoStuff () dependiera de i de alguna manera en el futuro, propondría esta variante garantizada de enmascaramiento de bits sin ramificaciones.
Donde popcount es cualquier función que realiza un recuento de población (número de bits de recuento = 1). Habrá cierta libertad para poner restricciones más avanzadas con i y sus vecinos. Si eso no es necesario, podemos quitar el bucle interno y rehacer el bucle externo
seguido de un
fuente
Además, si no le importa reordenar la colección, std :: partition es barato.
fuente
std::partition
reordena el contenedor.Estoy asombrado por la complejidad de las soluciones anteriores. Iba a sugerir uno simple
#define foreach(a,b,c,d) for(a; b; c)if(d)
pero tiene algunos déficits obvios, por ejemplo, debe recordar usar comas en lugar de punto y coma en su bucle, y no puede usar el operador de coma ena
oc
.fuente
Otra solución en caso de que las i: s sean importantes. Éste crea una lista que completa los índices para los que llamar a doStuff (). Una vez más, el punto principal es evitar la ramificación y canjearla por costos aritméticos en proceso.
La línea "mágica" es la línea de carga del búfer que calcula aritméticamente si para mantener el valor y permanecer en la posición o para contar la posición y agregar valor. Así que intercambiamos una rama potencial por algunas lógicas y aritméticas y tal vez algunos hits de caché. Un escenario típico en el que esto sería útil es si doStuff () hace una pequeña cantidad de cálculos que se pueden canalizar y cualquier rama entre llamadas podría interrumpir esos canales.
Luego simplemente recorra el búfer y ejecute doStuff () hasta que lleguemos a cnt. Esta vez tendremos el i actual almacenado en el búfer para que podamos usarlo en la llamada a doStuff () si es necesario.
fuente
Uno puede describir su patrón de código como la aplicación de alguna función a un subconjunto de un rango, o en otras palabras: aplicarlo al resultado de aplicar un filtro a todo el rango.
Esto se puede lograr de la manera más sencilla con la biblioteca de rangos-v3 de Eric Neibler ; aunque es un poco monstruoso, porque quieres trabajar con índices:
Pero si está dispuesto a renunciar a los índices, obtendrá:
que es mejor en mi humilde opinión.
PD: la biblioteca de rangos se dirige principalmente al estándar C ++ en C ++ 20.
fuente
Solo mencionaré a Mike Acton, definitivamente diría:
Si tiene que hacer eso, tiene un problema con sus datos. ¡Ordena tus datos!
fuente