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


Con casi todo el código que escribo, a menudo estoy lidiando con problemas de reducción de conjuntos en colecciones que finalmente terminan con condiciones ingenuas de "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 para 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 la opinión, 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?

Author: Rakete1111, 2016-07-15

11 answers

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 es molesto para usted, podría usar un for_each_if como 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;
  }
}

Usecase:

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

Demostración En Vivo

 95
Author: 101010,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-15 15:17:11

Boost proporciona rangos que se pueden usar w/ range-based for. Los rangos tienen la ventaja de que no copian la estructura de datos subyacente, simplemente proporcionan una 'vista' (es decir, begin(), end() para la gama y operator++(), operator==() para el iterador). Esto podría 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;
    }
}
 48
Author: lorro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-16 11:34:36

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 quieres un nuevo algoritmo, al menos reutiliza for_each allí 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); });
  }
 41
Author: Jonathan Wakely,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-15 15:55:43

La idea de evitar

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

Construye como un antipatrón es demasiado amplio.

Está completamente bien procesar varios elementos que coinciden con una determinada expresión desde dentro 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í el condicional se coloca mejor dentro del bucle, es decir,

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

Es muy preferible a

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

Se convierte en un antipattern 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 body se ejecute 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 if dentro de un for, pero desde el contexto queda claro lo que hace, no hay necesidad de cambiar este código a menos que la búsqueda cambie (por ejemplo, a un map), y es inmediatamente claro que create_object() se llama solo una vez, porque no está dentro de un bucle.

 21
Author: Simon Richter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-17 18:14:48

Aquí hay una función rápida relativamente mínima filter.

Toma un predicado. Devuelve un objeto de función que toma un iterable.

Devuelve un iterable que se puede usar en un bucle for(:).

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 debería hacer iteradores reales, no los pseudo-fascades que califican a for(:) 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 agradable, y las impresiones

1
3
5

En vivo ejemplo.

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

 17
Author: Yakk - Adam Nevraumont,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-16 00:39:01

Un estilo que se usa lo suficiente para mencionar, 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 de DoStuff(); cuando aumenta la complejidad de la condición. Lógicamente, DoStuff(); debería estar en el nivel superior del bucle for, y lo está.
  • Inmediatamente deja claro que el bucle itera sobre los SOMETHINGs de la colección, sin requerir que el lector verifique que no hay nada después del cierre } del if bloque.
  • No requiere bibliotecas ni macros o funciones auxiliares.

Desventajas:

  • continue, al igual que otras instrucciones de control de flujo, se usa mal de maneras que conducen a un código difícil de seguir tanto que algunas personas se oponen a cualquier uso de ellos: hay un estilo válido de codificación que algunos siguen que evita continue, que evita break que no sea en una switch, que evita return que no sea al final de una función.
 15
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-16 07:56:16
for(auto const &x: myCollection) if(x == something) doStuff();

Se parece bastante a una comprensión for específica de C++para mí. A usted?

 11
Author: bipll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-16 09:54:00

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

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 hace un recuento de población ( número de recuento de bits = 1 ). Habrá cierta libertad para poner restricciones más avanzadas con yo 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();
 7
Author: mathreadler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-23 09:12:08

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

#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
}
 6
Author: Loreto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-16 19:40:08

Estoy asombrado de la complejidad de las soluciones anteriores. Iba a sugerir un simple #define foreach(a,b,c,d) for(a; b; c)if(d) pero tiene algunos déficits obvios, por ejemplo, tienes que recordar usar comas en lugar de punto y coma en tu bucle, y no puedes usar el operador de coma en a o 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;
}
 5
Author: Ian Parberry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-16 17:36:51

Otra solución en caso de que las i:s sean importantes. Esta crea una lista que rellena los índices para los que llamar a doStuff (). Una vez más, el punto principal es evitar la ramificación y cambiarla por costos aritméticos canalizables.

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 posición o para contar la posición y agregar valor. Así que cambiamos una rama potencial para algunas lógicas y aritmética y tal vez algunos cache hits. Un escenario típico en el que esto sería útil es si doStuff() hace una pequeña cantidad de cálculos canalizables y cualquier rama entre llamadas podría interrumpir esas canalizaciones.

A continuación, simplemente loop sobre el búfer y ejecutar doStuff() hasta llegar a cnt. Esta vez tendremos la corriente almacenada en el buffer, de modo que se puede utilizar en la llamada a doStuff() si sería necesario.

 2
Author: mathreadler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-25 14:27:29