¿Cómo puedo evitar bucles "for" con una condición "si" dentro de ellos con C ++?

111

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?

Oscurecer
fuente
7
Fuera de la funcionalidad de la biblioteca estándar de C ++, puede probar std::copy_if, pero las selecciones no son perezosas
milleniumbug
14
Puede que le interese range-v3 . También debería llegar a C ++ como TS y, con suerte, estandarizado en una versión futura.
NathanOliver
12
Siento la necesidad de señalar que el ifinterior forque 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 que DoStuffclaramente tiene efectos secundarios.
Pharap
60
Nunca he entendido realmente por qué las personas vinculan la combinación de toda la lógica en una sola línea para que se vea mejor o más legible. Su fragmento de C ++ en la parte superior es, con mucho, el más legible para mí de todas sus posibilidades. Y dado que la eficiencia no cambiará, no puedo entender por qué prefiere no escribir eso, a menos que le paguen por la cantidad de líneas de código que elimine.
Cody Gray
10
@CodyGray De acuerdo: es 'simplemente azúcar sintáctico. Y el título de la pregunta es engañoso, porque es muy diferente evitando ramificarlo y esconderlo bajo la abstracción.
edmz

Respuestas:

99

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_ifcomo el siguiente:

template<typename Iter, typename Pred, typename Op> 
void for_each_if(Iter first, Iter last, Pred p, Op op) {
  while(first != last) {
    if (p(*first)) op(*first);
    ++first;
  }
}

Caso de uso:

std::vector<int> v {10, 2, 10, 3};
for_each_if(v.begin(), v.end(), [](int i){ return i > 5; }, [](int &i){ ++i; });

Demo en vivo

101010
fuente
10
Eso es excepcionalmente inteligente. También estaré de acuerdo en que no es sencillo y probablemente solo usaré las condiciones if al programar C ++ que otros consumen. ¡Pero eso es exactamente lo que necesito para mi uso personal! :)
Darkenor
14
@Default Pasar pares de iteradores en lugar de contenedores es C ++ más flexible e idiomático.
Mark B
8
@Slava, en general, los rangos no reducirán la cantidad de algoritmos. Por ejemplo, todavía necesita find_ify findsi funcionan en rangos o pares de iteradores. (Hay algunas excepciones, como for_eachy for_each_n). La forma de evitar escribir nuevos algos para cada estornudo es usar diferentes operaciones con los algos existentes, por ejemplo, en lugar de for_each_ifincrustar la condición en el invocable pasado a for_each, por ejemplofor_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Jonathan Wakely
9
Tendré que estar de acuerdo con la primera oración: la solución estándar para-si es mucho más legible y más fácil de trabajar. Creo que la sintaxis lambda y el uso de una plantilla definida en otro lugar solo para manejar un bucle simple irritaría o posiblemente confundiría a otros desarrolladores. Estás sacrificando localidad y rendimiento por ... ¿qué? ¿Poder escribir algo en una línea?
user1354557
45
Cough @Darkenor, en general, la programación " excepcionalmente inteligente" debe evitarse porque molesta a todos los demás, incluido tu yo futuro.
Ryan
48

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 y operator++(), operator==()para el iterador). Esto puede ser de su interés: http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.html

#include <boost/range/adaptor/filtered.hpp>
#include <iostream>
#include <vector>

struct is_even
{
    bool operator()( int x ) const { return x % 2 == 0; }
};

int main(int argc, const char* argv[])
{
    using namespace boost::adaptors;

    std::vector<int> myCollection{1,2,3,4,5,6,7,8,9};

    for( int i: myCollection | filtered( is_even() ) )
    {
        std::cout << i;
    }
}
lorro
fuente
1
¿Puedo sugerir usar el ejemplo de OP en su lugar, es decir, is_even=> condition, input=> myCollectionetc.
defecto el
Esta es una respuesta bastante excelente y definitivamente lo que estoy buscando hacer. Voy a postergar la aceptación a menos que alguien pueda encontrar una forma estándar de hacerlo que use ejecución diferida / diferida. Voto a favor.
Darkenor
5
@Darkenor: Si Boost es un problema para usted (por ejemplo, tiene prohibido usarlo debido a la política de la empresa y la sabiduría del gerente), puedo encontrar una definición simplificada filtered()para usted; dicho esto, es mejor usar una lib compatible que un código ad-hoc.
lorro
Totalmente de acuerdo contigo. Lo acepté porque la forma compatible con el estándar que vino primero porque la pregunta estaba orientada a C ++ en sí, no a la biblioteca boost. Pero esto es realmente excelente. Además, sí, lamentablemente he trabajado en muchos lugares que prohibieron Boost por razones absurdas ...
Darkenor
@LeeClagett:? .
lorro
44

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:

std::for_each(first, last, [](auto&& x){ if (cond(x)) { ... } });

O si realmente desea un nuevo algoritmo, al menos reutilícelo for_eachallí en lugar de duplicar la lógica de iteración:

template<typename Iter, typename Pred, typename Op> 
  void
  for_each_if(Iter first, Iter last, Pred p, Op op) {
    std::for_each(first, last, [&](auto& x) { if (p(x)) op(x); });
  }
Jonathan Wakely
fuente
Mucho mejor y más claro para usar la biblioteca estándar.
anónimo
4
¿Porque std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });es totalmente más simple que for (Iter x = first; x != last; x++) if (p(x)) op(x);}?
usuario253751
2
@immibis reutilizar la biblioteca estándar tiene otros beneficios, como la verificación de la validez del iterador, o (en C ++ 17) ser mucho más fácil de paralelizar, simplemente agregando un argumento más: std::for_each(std::execution::par, first, last, ...);¿Qué tan fácil es agregar esas cosas a un bucle escrito a mano?
Jonathan Wakely
1
#pragma omp paralelo para
Mark K Cowan
2
@mark lo siento, alguna peculiaridad aleatoria de su código fuente o cadena de compilación hizo que esa extensión del compilador paralelo no estándar, frágil y molesto, generara un aumento de rendimiento cero sin diagnóstico.
Yakk - Adam Nevraumont
21

La idea de evitar

for(...)
    if(...)

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

for(...)
    if(...)
        do_process(...);

es mucho mejor que

for(...)
    maybe_process(...);

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.

for(int i = 0; i < size; ++i)
    if(i == 5)

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

for(creator &c : creators)
    if(c.name == requested_name)
    {
        unique_ptr<object> obj = c.create_object();
        obj.owner = this;
        return std::move(obj);
    }

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:

creator &lookup(string const &requested_name)
{
    for(creator &c : creators)
        if(c.name == requested_name)
            return c;
}

creator &c = lookup(requested_name);
unique_ptr obj = c.create_object();

Todavía hay un ifdentro de a for, 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 a map), y está inmediatamente claro que create_object()se llama solo una vez, porque es no dentro de un bucle.

Simon Richter
fuente
Me gusta esto, como una visión general reflexiva y equilibrada, incluso si en cierto sentido se niega a responder la pregunta planteada. Encuentro que el 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.
PJTraill
@PJTraill, la forma en que se formuló la pregunta me recordó la perorata de Raymond Chen contra el antipatrón por si , que ha sido cargado y de alguna manera se convirtió en un absoluto. Estoy totalmente de acuerdo en que a for(...) if(...) { ... }menudo es la mejor opción (por eso califiqué la recomendación de dividir la acción en una subrutina).
Simon Richter
1
Gracias por el enlace, que me aclaró las cosas: el nombre " para-si " es engañoso y debería ser algo como " para-todos-si-uno " o " evitar-buscar ". Me recuerda la forma en que Wikipedia describió la inversión de abstracción en 2005 como cuando uno “ crea construcciones simples sobre las complejas (unas)”, ¡hasta que la reescribí! En realidad, ni siquiera me apresuraría a arreglar la forma de búsqueda-proceso-salida de for(…)if(…)…si fuera el único lugar donde se produjo la búsqueda.
PJTraill
17

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.

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  bool empty() const { return begin()==end(); }
};
template<class It>
range_t<It> range( It b, It e ) { return {std::move(b), std::move(e)}; }

template<class It, class F>
struct filter_helper:range_t<It> {
  F f;
  void advance() {
    while(true) {
      (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      if (this->empty())
        return;
      if (f(*this->begin()))
        return;
    }
  }
  filter_helper(range_t<It> r, F fin):
    range_t<It>(r), f(std::move(fin))
  {
      while(true)
      {
          if (this->empty()) return;
          if (f(*this->begin())) return;
          (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      }
  }
};

template<class It, class F>
struct filter_psuedo_iterator {
  using iterator_category=std::input_iterator_tag;
  filter_helper<It, F>* helper = nullptr;
  bool m_is_end = true;
  bool is_end() const {
    return m_is_end || !helper || helper->empty();
  }

  void operator++() {
    helper->advance();
  }
  typename std::iterator_traits<It>::reference
  operator*() const {
    return *(helper->begin());
  }
  It base() const {
      if (!helper) return {};
      if (is_end()) return helper->end();
      return helper->begin();
  }
  friend bool operator==(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    if (lhs.is_end() && rhs.is_end()) return true;
    if (lhs.is_end() || rhs.is_end()) return false;
    return lhs.helper->begin() == rhs.helper->begin();
  }
  friend bool operator!=(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    return !(lhs==rhs);
  }
};
template<class It, class F>
struct filter_range:
  private filter_helper<It, F>,
  range_t<filter_psuedo_iterator<It, F>>
{
  using helper=filter_helper<It, F>;
  using range=range_t<filter_psuedo_iterator<It, F>>;

  using range::begin; using range::end; using range::empty;

  filter_range( range_t<It> r, F f ):
    helper{{r}, std::forward<F>(f)},
    range{ {this, false}, {this, true} }
  {}
};

template<class F>
auto filter( F&& f ) {
    return [f=std::forward<F>(f)](auto&& r)
    {
        using std::begin; using std::end;
        using iterator = decltype(begin(r));
        return filter_range<iterator, std::decay_t<decltype(f)>>{
            range(begin(r), end(r)), f
        };
    };
};

Tomé atajos. Una biblioteca real debe hacer iteradores reales, no losfor(:) pseudofascadas de calificación que hice.

En el punto de uso, se ve así:

int main()
{
  std::vector<int> test = {1,2,3,4,5};
  for( auto i: filter([](auto x){return x%2;})( test ) )
    std::cout << i << '\n';
}

que es bastante bonito y se imprime

1
3
5

Ejemplo vivo .

Hay una adición propuesta a C ++ llamada Rangesv3 que hace este tipo de cosas y más. boosttambién tiene rangos de filtro / iteradores disponibles. boost también tiene ayudantes que hacen que escribir lo anterior sea mucho más corto.

Yakk - Adam Nevraumont
fuente
15

Un estilo que se acostumbra lo suficiente como para mencionarlo, pero que aún no se ha mencionado, es:

for(int i=0; i<myCollection.size(); i++) {
  if (myCollection[i] != SOMETHING)
    continue;

  DoStuff();
}

Ventajas:

  • No cambia el nivel de sangría 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.
  • Inmediatamente deja claro que el ciclo itera sobre los SOMETHINGs de la colección, sin requerir que el lector verifique que no hay nada después del cierre }de la colección.if bloque.
  • No requiere bibliotecas ni macros o funciones auxiliares.

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 evita continue, que evita breakque no sea en a switch, que evita returnotro que al final de una función.

fuente
3
Yo diría que en un forbucle 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 la fordeclaración se lea bien y, como dijiste, no sangra los aspectos funcionales restantes del ciclo. continueSin embargo, si está más abajo, se sacrifica algo de claridad (es decir, si siempre se realizará alguna operación antes de la ifdeclaración).
anónimo
11
for(auto const &x: myCollection) if(x == something) doStuff();

forPara mí, se parece mucho a una comprensión específica de C ++ . ¿Para ti?

bipll
fuente
No creo que la palabra clave auto estuviera presente antes de c ++ 11, así que no diría que es c ++ muy clásico. Si puedo hacer una pregunta aquí en el comentario, ¿"auto const" le diría al compilador que puede reorganizar todos los elementos como quiera? Tal vez sea más fácil para el compilador planificar evitar la ramificación si ese es el caso.
mathreadler
1
@mathreadler Cuanto antes la gente deje de preocuparse por el "c ++ clásico", mejor. C ++ 11 fue un evento macroevolucionario para el lenguaje y tiene 5 años: debería ser el mínimo por el que nos esforzamos. De todos modos, el OP etiquetó eso y C ++ 14 (¡incluso mejor!). No, auto constno tiene nada que ver con el orden de iteración. Si busca basado en forrangos, verá que básicamente hace un bucle estándar de begin()a end()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 Tierra
subrayado_d
1
@mathreadler, en realidad lo era, simplemente tenía un significado bastante diferente. Lo que no estaba presente es range-for ... y cualquier otra característica distinta de C ++ 11. Lo que quise decir aquí es que range-fors, std::futures, std::functions, 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.
bipll
@underscore_d, un compilador puede realizar cualquier transformación siempre que se obedezca la regla como-si, ¿no es así?
bipll
1
Hmmm, ¿y qué se puede querer decir con eso?
bipll
7

Si DoStuff () dependiera de i de alguna manera en el futuro, propondría esta variante garantizada de enmascaramiento de bits sin ramificaciones.

unsigned int times = 0;
const int kSize = sizeof(unsigned int)*8;
for(int i = 0; i < myCollection.size()/kSize; i++){
  unsigned int mask = 0;
  for (int j = 0; j<kSize; j++){
    mask |= (myCollection[i*kSize+j]==SOMETHING) << j;
  }
  times+=popcount(mask);
}

for(int i=0;i<times;i++)
   DoStuff();

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

for(int i = 0; i < myCollection.size(); i++)
  times += (myCollection[i]==SOMETHING);

seguido de un

for(int i=0;i<times;i++)
   DoStuff();
matemático
fuente
6

Además, si no le importa reordenar la colección, std :: partition es barato.

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

void DoStuff(int i)
{
    std::cout << i << '\n';
}

int main()
{
    using namespace std::placeholders;

    std::vector<int> v {1, 2, 5, 0, 9, 5, 5};
    const int SOMETHING = 5;

    std::for_each(v.begin(),
                  std::partition(v.begin(), v.end(),
                                 std::bind(std::equal_to<int> {}, _1, SOMETHING)), // some condition
                  DoStuff); // action
}
Loreto
fuente
Pero std::partitionreordena el contenedor.
celtschk
5

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 en ao c.

#include <list>
#include <iostream>

using namespace std; 

#define foreach(a,b,c,d) for(a; b; c)if(d)

int main(){
  list<int> a;

  for(int i=0; i<10; i++)
    a.push_back(i);

  for(auto i=a.begin(); i!=a.end(); i++)
    if((*i)&1)
      cout << *i << ' ';
  cout << endl;

  foreach(auto i=a.begin(), i!=a.end(), i++, (*i)&1)
    cout << *i << ' ';
  cout << endl;

  return 0;
}
Ian Parberry
fuente
3
La complejidad de algunas respuestas solo es alta porque primero muestran un método genérico reutilizable (que haría solo una vez) y luego lo usa. No es efectivo si tiene un bucle con una condición if en toda su aplicación, pero es muy efectivo si ocurre mil veces.
gnasher729
1
Como la mayoría de las sugerencias, esto hace que sea más difícil, no más fácil, identificar el rango y la condición de selección. Y el uso de una macro aumenta la incertidumbre sobre cuándo (y con qué frecuencia) se evalúan las expresiones, incluso si no hay sorpresas aquí.
PJTraill
2

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.

int buffer[someSafeSize];
int cnt = 0; // counter to keep track where we are in list.
for( int i = 0; i < container.size(); i++ ){
   int lDecision = (container[i] == SOMETHING);
   buffer[cnt] = lDecision*i + (1-lDecision)*buffer[cnt];
   cnt += lDecision;
}

for( int i=0; i<cnt; i++ )
   doStuff(buffer[i]); // now we could pass the index or a pointer as an argument.

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.

matemático
fuente
1

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:

using namespace ranges;
auto mycollection_has_something = 
    [&](std::size_t i) { return myCollection[i] == SOMETHING };
auto filtered_view = 
    views::iota(std::size_t{0}, myCollection.size()) | 
    views::filter(mycollection_has_something);
for (auto i : filtered_view) { DoStuff(); }

Pero si está dispuesto a renunciar a los índices, obtendrá:

auto is_something = [&SOMETHING](const decltype(SOMETHING)& x) { return x == SOMETHING };
auto filtered_collection = myCollection | views::filter(is_something);
for (const auto& x : filtered_collection) { DoStuff(); }

que es mejor en mi humilde opinión.

PD: la biblioteca de rangos se dirige principalmente al estándar C ++ en C ++ 20.

einpoklum
fuente
0

Solo mencionaré a Mike Acton, definitivamente diría:

Si tiene que hacer eso, tiene un problema con sus datos. ¡Ordena tus datos!

v.oddou
fuente