112 votes

Comment éviter les boucles "for" avec une condition "if" à l'intérieur en C++ ?

Dans presque tout le code que j'écris, je suis souvent confronté à des problèmes de réduction d'ensembles sur des collections qui finissent par contenir des conditions "if" naïves. 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 collection (facilement) et ensuite effectuer toutes les opérations sur mon ensemble réduit. En pseudo-code :

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

Et dans d'autres variantes du C, comme le C#, je pourrais réduire avec une clause where du type

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 style subjectif/opinionnel, mais je ne peux m'empêcher de penser qu'il me manque quelque chose de vraiment fondamental qui pourrait me permettre d'utiliser cette technique privilégiée avec C++. Quelqu'un pourrait-il m'éclairer ?

7 votes

En dehors des fonctionnalités de la bibliothèque standard C++, vous pouvez essayer std::copy_if mais les sélections ne sont pas paresseuses.

0 votes

Si vous êtes d'accord pour faire une copie, vous devriez jeter un coup d'oeil à std::copy_if

14 votes

Vous pourriez être intéressé par gamme-v3 . Il devrait également arriver en C++ en tant que TS et, espérons-le, être normalisé dans une prochaine version.

100voto

40two Points 8224

À mon avis, il est plus simple et plus lisible d'utiliser une boucle for avec un if à l'intérieur. Toutefois, si cela vous ennuie, vous pouvez utiliser une boucle de type 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;
  }
}

Cas d'utilisation :

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

Démonstration en direct

10 votes

C'est-à-dire exceptionnellement intelligente. Je suis également d'accord pour dire que ce n'est pas simple et que je me contenterai probablement d'utiliser des conditions if lorsque je programmerai du C++ qui sera utilisé par d'autres. Mais c'est exactement ce dont j'ai besoin pour mon usage personnel ! :)

1 votes

Serait-il possible de passer simplement dans v et utiliser std::begin et std::end dans le for_each_if fonction ?

14 votes

@Default Passer des paires d'itérateurs plutôt que des conteneurs est à la fois plus flexible et plus idiomatique pour le C++.

48voto

lorro Points 1220

Boost fournit des gammes qui peuvent être utilisées avec la gamme basée pour. Les intervalles ont l'avantage de ne pas copier la structure de données sous-jacente, ils fournissent simplement une "vue" (c'est-à-dire, begin() , end() pour la gamme et operator++() , operator==() pour l'itérateur). Ceci 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;
    }
}

1 votes

Puis-je suggérer d'utiliser l'exemple de l'OP à la place, à savoir is_even => condition , input => myCollection etc.

0 votes

C'est une excellente réponse et c'est certainement ce que je cherche à faire. Je vais attendre avant de l'accepter, à moins que quelqu'un ne propose un moyen standard de le faire en utilisant l'exécution paresseuse/différée. Upvoted.

5 votes

@Darkenor : Si Boost est un problème pour vous (par exemple, vous êtes interdit de l'utiliser en raison de la politique de l'entreprise et de la sagesse du manager), je peux proposer une définition simplifiée de filtered() pour vous - ceci dit, il est préférable d'utiliser une librairie supportée plutôt qu'un code ad-hoc.

46voto

Jonathan Wakely Points 45593

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

0 votes

Bien meilleur et plus clair pour l'utilisation de la bibliothèque standard.

4 votes

Parce que std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); }); est totalement plus simple que for (Iter x = first; x != last; x++) if (p(x)) op(x);} ?

2 votes

@immibis réutilisant la bibliothèque standard présente d'autres avantages, comme la vérification de la validité des itérateurs, ou (en C++17) le fait d'être beaucoup plus facile à paralléliser, simplement en ajoutant un argument supplémentaire : std::for_each(std::execution::par, first, last, ...); Est-il facile d'ajouter ces éléments à une boucle écrite à la main ?

21voto

Simon Richter Points 11471

L'idée d'éviter

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

comme un anti-modèle est trop large.

Il est tout à fait possible de traiter plusieurs éléments qui correspondent à une certaine expression à l'intérieur d'une boucle, et le code ne peut pas être plus clair que cela. Si le traitement devient trop volumineux pour tenir à l'écran, c'est une bonne raison d'utiliser une sous-routine, mais il est toujours préférable de placer la conditionnelle à l'intérieur de la boucle, à savoir

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

est largement préférable à

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

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

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

en est un exemple extrême et évident. Plus subtil, et donc plus commun, est un modèle de fabrique 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 sera exécuté une seule fois. Dans ce cas, il serait préférable de séparer le lookup :

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 Il n'est pas nécessaire de modifier ce code, à moins que la recherche ne change (par exemple, pour un map ), et il est immédiatement clair que create_object() n'est appelé qu'une seule fois, car il n'est pas à l'intérieur d'une boucle.

0 votes

J'aime bien cet aperçu réfléchi et équilibré, même s'il refuse en un sens de répondre à la question posée. Je trouve que le for( range ){ if( condition ){ action } } -Ce style permet de lire facilement les choses, un morceau à la fois, et n'utilise que les connaissances des constructions de base du langage.

0 votes

@PJTraill, la façon dont la question a été formulée m'a fait penser à Raymond Chen s'insurge contre l'anti-modèle "for-if". qui a fait l'objet d'un culte et est devenu en quelque sorte un absolu. Je suis tout à fait d'accord pour dire que for(...) if(...) { ... } est souvent le meilleur choix (c'est pourquoi j'ai nuancé la recommandation de diviser l'action en une sous-routine).

1 votes

Merci pour le lien, qui m'a permis de clarifier les choses : le nom " pour-si "est trompeur, et devrait être quelque chose comme " pour-tout-le-monde " ou " recherche-évitement ". Cela me rappelle la façon dont Inversion d'abstraction a été décrite par Wikipedia en 2005 comme lorsqu'on " crée des constructions simples par-dessus des constructions complexes (ones)" - jusqu'à ce que je le réécrive ! En fait, je ne me précipiterais même pas pour réparer la forme lookup-process-exit de for(…)if(…)… s'il s'agissait du seul endroit où la recherche était effectuée.

17voto

Yakk Points 31636

Voici un bref aperçu relativement minimal filter fonction.

Elle prend un prédicat. Elle retourne un objet fonction qui prend un itérable.

Elle renvoie un itérable qui peut être utilisé dans une for(:) boucle.

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 devrait faire de vrais itérateurs, pas les for(:) - des pseudo-fascades qualifiantes 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';
}

ce qui est plutôt bien, et imprime

1
3
5

Exemple concret .

Il existe une proposition d'ajout au C++ appelée Rangesv3 qui fait ce genre de choses et plus encore. boost dispose également de plages de filtrage et d'itérateurs. boost a également des aides qui permettent d'écrire ce qui précède beaucoup plus rapidement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X