web-dev-qa-db-fra.com

Comment puis-je éviter les boucles "pour" avec une condition "if" avec C ++?

Avec presque tout le code que j'écris, je suis souvent confronté à des problèmes de réduction des ensembles sur des collections qui finissent par se retrouver avec des conditions "si" naïves à l'intérieur de celles-ci. Voici un exemple simple:

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

Avec les langages fonctionnels, je peux résoudre le problème en réduisant la collection à une autre (facilement), puis en effectuant toutes les opérations sur mon ensemble réduit. En pseudocode:

newCollection <- myCollection where <x=true
map DoStuff newCollection

Et dans d'autres variantes C, comme C #, je pourrais réduire avec une clause where telle que

foreach (var x in myCollection.Where(c=> c == SOMETHING)) 
{
   DoStuff();
}

Ou mieux (du moins à mes yeux)

myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));

Certes, je fais beaucoup de mélange de paradigmes et de styles subjectif/basé sur les opinions, mais je ne peux pas m'empêcher de penser qu'il me manque quelque chose de fondamental qui pourrait me permettre d'utiliser cette technique privilégiée avec C++. Quelqu'un pourrait-il m'éclairer?

110
Darkenor

À mon humble avis, il est plus simple et plus lisible d’utiliser une boucle for avec un if à l’intérieur. Toutefois, si cela vous gêne, vous pouvez utiliser un for_each_if comme celui ci-dessous:

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; });

Démo en direct

98
101010

Boost fournit des gammes qui peuvent être utilisées avec/pour une plage donnée. Les plages ont l’avantage de ne pas copier la structure de données sous-jacente, elles fournissent simplement une "vue" (c’est-à-dire, begin(), end() pour la plage et operator++(), operator==() pour l'itérateur). Cela pourrait vous intéresser: 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
lorro

Au lieu de créer un nouvel algorithme, comme le fait la réponse acceptée, vous pouvez utiliser un algorithme existant avec une fonction qui applique la condition:

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

Ou si vous voulez vraiment un nouvel algorithme, réutilisez au moins for_each au lieu de dupliquer la logique d’itération:

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); });
  }
44
Jonathan Wakely

L'idée d'éviter

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

construit comme un anti-modèle est trop large.

Il est tout à fait judicieux de traiter plusieurs éléments correspondant à une expression donnée à l’intérieur d’une boucle et le code ne peut pas être beaucoup plus clair. Si le traitement devient trop volumineux pour tenir sur l’écran, c’est une bonne raison d’utiliser un sous-programme, mais il est néanmoins préférable de placer le conditionnel dans la boucle, c.-à-d.

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

est grandement préférable à

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

Cela devient un antipattern lorsqu'un seul élément correspond, car il serait alors plus clair de rechercher d'abord l'élément et d'effectuer le traitement en dehors de la boucle.

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

est un exemple extrême et évident de cela. Plus subtil, et donc plus commun, est un motif d'usine comme

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

C'est difficile à lire, car il n'est pas évident que le code du corps ne sera exécuté qu'une seule fois. Dans ce cas, il serait préférable de séparer la recherche:

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();

Il y a toujours un if dans un for, mais à partir du contexte, il devient clair ce qu'il fait, il n'est pas nécessaire de changer ce code à moins que la recherche ne change (par exemple en un map), et il est immédiatement évident que create_object() n'est appelé qu'une seule fois, car il ne se trouve pas dans une boucle.

21
Simon Richter

Voici une fonction rapide et relativement minime filter.

Il faut un prédicat. Il retourne un objet de fonction qui prend un itérable.

Il retourne une variable pouvant être utilisée dans une boucle 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
        };
    };
};

J'ai pris des raccourcis. Une vraie bibliothèque doit faire de vrais itérateurs, pas les pseudo-fascades qualifiantes for(:) que j'ai faites.

Au point d'utilisation, cela ressemble à ceci:

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';
}

qui est jolie Nice, et imprime

1
3
5

Exemple en direct .

Il y a un ajout proposé à C++ appelé Rangesv3 qui fait ce genre de choses et plus. boost dispose également de plages de filtres/itérateurs. boost a également des aides qui rendent l’écriture ci-dessus beaucoup plus courte.

17

Un style qui s'habitue assez pour être mentionné, mais qui n'a pas encore été mentionné, est:

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

  DoStuff();
}

Avantages:

  • Ne modifie pas le niveau d'indentation de DoStuff(); lorsque la complexité de la condition augmente. Logiquement, DoStuff(); devrait être au plus haut niveau de la boucle for, et c'est le cas.
  • Indique immédiatement que la boucle itère sur le SOMETHINGs de la collection, sans demander au lecteur de vérifier qu’il n’ya rien après la fermeture } Du bloc if.
  • Ne nécessite aucune bibliothèque, macros ou fonctions auxiliaires.

Désavantages:

  • continue, comme d’autres instructions de contrôle de flux, est utilisé de manière abusive de manière à conduire à un code difficile à suivre si bien que certaines personnes s’opposent à toute leur utilisation: il existe un code valide. style de codage que certains suivent qui évite continue, qui évite break autrement que dans un switch, qui évite return autrement qu'à la fin d'une fonction .
15
user743382
for(auto const &x: myCollection) if(x == something) doStuff();

Cela ressemble beaucoup à une compréhension for spécifique à C++. À toi?

11
bipll

Si DoStuff () dépendrait de i d'une manière ou d'une autre à l'avenir, je proposerais cette variante de masquage de bits sans branche garantie.

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();

Où popcount est une fonction qui effectue un dénombrement de la population (nombre de bits = 1). Il y aura une certaine liberté pour imposer des contraintes plus avancées avec i et leurs voisins. Si cela n'est pas nécessaire, nous pouvons supprimer la boucle interne et refaire la boucle externe.

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

suivi d'un

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

De plus, si vous ne vous souciez pas de réorganiser la collection, std :: partition est bon marché.

#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
Loreto

Je suis impressionné par la complexité des solutions ci-dessus. J'allais suggérer une #define foreach(a,b,c,d) for(a; b; c)if(d) simple, mais elle présente quelques déficits évidents. Par exemple, vous devez vous rappeler d'utiliser des virgules au lieu de points-virgules dans votre boucle et vous ne pouvez pas utiliser l'opérateur de virgule dans a ou 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
Ian Parberry

Une autre solution au cas où les i: s sont importants. Celui-ci construit une liste qui remplit les index pour lesquels appeler doStuff (). Une fois encore, l’essentiel est d’éviter la création de succursales et de les échanger contre des coûts arithmétiques viables.

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 ligne "magique" est la ligne de chargement de la mémoire tampon qui calcule de manière arithmétique si vous souhaitez conserver la valeur et rester en position ou pour compter la position et ajouter de la valeur. Nous avons donc échangé une branche potentielle contre certaines logiques et arithmétiques et peut-être même quelques succès en cache. Un scénario typique dans lequel cela serait utile est que si doStuff () effectue une petite quantité de calculs pouvant être ciblés et que toute branche entre des appels puisse interrompre ces pipelines.

Ensuite, passez simplement sur le tampon et exécutez doStuff () jusqu'à atteindre cnt. Cette fois, nous aurons le courant i stocké dans la mémoire tampon afin que nous puissions l’utiliser dans l’appel de doStuff () si nous en avions besoin.

2
mathreadler