web-dev-qa-db-fra.com

pour loop vs std :: for_each avec lambda

Considérons une fonction de modèle écrite en C++ 11 qui itère sur un conteneur . Veuillez exclure de la considération la syntaxe de la boucle de plage car elle n’est pas encore prise en charge par le compilateur avec lequel je travaille.

template <typename Container>
void DoSomething(const Container& i_container)
  {
  // Option #1
  for (auto it = std::begin(i_container); it != std::end(i_container); ++it)
    {
    // do something with *it
    }

  // Option #2
  std::for_each(std::begin(i_container), std::end(i_container), 
    [] (typename Container::const_reference element)
    {
    // do something with element
    });
  }

Quels sont les avantages/inconvénients de for loop vs std::for_each en termes de:

une performance? (Je ne m'attends à aucune différence)

b) lisibilité et maintenabilité?

Ici, je vois de nombreux inconvénients de for_each. Il n'accepterait pas un tableau de style c tant que la boucle l'accepterait. La déclaration du paramètre formel lambda est très détaillée, il est donc impossible d’utiliser auto. Il n'est pas possible de sortir de for_each.

Avant 11 jours C++, les arguments contre for nécessitaient de spécifier le type de l'itérateur (ne tient plus) et une possibilité facile de ne pas taper correctement la condition de boucle (je n'ai jamais commis une telle erreur depuis 10 ans). .

En conclusion, mes pensées sur for_each contredisent l’opinion commune. Qu'est-ce que j'oublie ici?

36
Andrey

Je pense qu'il y a d'autres différences non encore couvertes par les réponses jusqu'à présent.

  1. un for_each peut accepter tout objet appelable approprié, permettant ainsi de "recycler" le corps de la boucle pour différentes boucles for. Par exemple (pseudo-code)

    for( range_1 ) { lengthy_loop_body }    // many lines of code
    for( range_2 ) { lengthy_loop_body }    // the same many lines of code again
    

    devient

    auto loop_body = some_lambda;           // many lines of code here only
    std::for_each( range_1 , loop_body );   // a single line of code
    std::for_each( range_2 , loop_body );   // another single line of code
    

    évitant ainsi les duplications et simplifiant la maintenance du code. (Bien entendu, dans un drôle de mélange de styles, on pourrait également utiliser une approche similaire avec la boucle for.)

  2. une autre différence concerne la rupture de la boucle (avec break ou return dans la boucle for). Autant que je sache, dans une boucle for_each, cela ne peut être fait qu’en levant une exception. Par exemple

    for( range )
    {
      some code;
      if(condition_1) return x; // or break
      more code;
      if(condition_2) continue;
      yet more code;
    }
    

    devient

    try {
      std::for_each( range , [] (const_reference x)
                    {
                      some code;
                      if(condition_1) throw x;
                      more code;
                      if(condition_2) return;
                      yet more code;
                    } );
    } catch(const_reference r) { return r; }
    

    avec les mêmes effets concernant l’appel de destructeurs pour les objets ayant la portée du corps de la boucle et du corps de la fonction (autour de la boucle).

  3. le principal avantage de for_each est, à mon humble avis, que l’on peut le surcharger pour certains types de conteneurs, lorsque l’itération simple n’est pas aussi efficace. Par exemple, considérons un conteneur contenant une liste chaînée de blocs de données, chaque bloc contenant un tableau d'éléments contigus, similaire à (en omettant le code non pertinent).

    namespace my {
      template<typename data_type, unsigned block_size>
      struct Container
      {
        struct block
        {
          const block*NEXT;
          data_type DATA[block_size];
          block() : NEXT(0) {}
        } *HEAD;
      };
    }
    

    alors, un itérateur avant approprié pour ce type devrait vérifier la fin du bloc à chaque incrément et l'opérateur de comparaison doit comparer le pointeur de bloc et l'index dans chaque bloc (en omettant le code non pertinent):

    namespace my {
      template<typename data_type, unsigned block_size>
      struct Container
      {
        struct iterator
        {
          const block*B;
          unsigned I;
          iterator() = default;
          iterator&operator=(iterator const&) = default;
          iterator(const block*b, unsigned i) : B(b), I(i) {}
          iterator& operator++()
          {
            if(++I==block_size) { B=B->NEXT; I=0; }    // one comparison and branch
            return*this;
          }
          bool operator==(const iterator&i) const
          { return B==i.B && I==i.I; }                 // one or two comparisons
          bool operator!=(const iterator&i) const
          { return B!=i.B || I!=i.I; }                 // one or two comparisons
          const data_type& operator*() const
          { return B->DATA[I]; }
        };
        iterator begin() const
        { return iterator(HEAD,0); }
        iterator end() const
        { return iterator(0,0); }
      };
    }
    

    ce type d'itérateur fonctionne correctement avec for et for_each, par exemple

    my::Container<int,5> C;
    for(auto i=C.begin();
        i!=C.end();              // one or two comparisons here
        ++i)                     // one comparison here and a branch
      f(*i);
    

    mais nécessite deux à trois comparaisons par itération ainsi qu’une branche. Une méthode plus efficace consiste à surcharger la fonction for_each() pour boucler le pointeur du bloc et l'index séparément:

    namespace my {
      template<typename data_type, int block_size, typename FuncOfDataType>
      FuncOfDataType&&
      for_each(typename my::Container<data_type,block_size>::iterator i,
               typename my::Container<data_type,block_size>::iterator const&e,
               FuncOfDataType f)
      {
        for(; i.B != e.B; i.B++,i.I=0)
          for(; i.I != block_size; i.I++)
            f(*i);
        for(; i.I != e.I; i.I++)
          f(*i);
        return std::move(f);
      }
    }
    using my::for_each;     // ensures that the appropriate
    using std::for_each;    // version of for_each() is used
    

    qui ne nécessite qu'une seule comparaison pour la plupart des itérations et ne comporte aucune branche (notez que les branches peuvent avoir un impact désastreux sur les performances). Notez que nous n'avons pas besoin de définir cela dans l'espace de noms std (ce qui peut être illégal), mais nous pouvons nous assurer que la version correcte est utilisée par les directives using appropriées. Cela équivaut à using std::swap; lors de la spécialisation de swap() pour certains types définis par l'utilisateur.

32
Walter

En ce qui concerne les performances, votre boucle for appelle std::end à plusieurs reprises, contrairement à std::for_each. Cela peut entraîner ou non une différence de performance en fonction du conteneur utilisé.

7
eq-
  • La version std::for_each visitera chaque élément exactement une fois. Une personne qui lit le code peut le savoir dès qu’ils voient std::for_each, car rien ne peut être fait dans le lambda pour déranger l’itérateur. Dans la boucle for traditionnelle, vous devez étudier le corps de la boucle pour rechercher un flux de contrôle inhabituel (continue, break, return) et une liaison avec l'itérateur (par exemple, dans ce cas, ignorez l'élément suivant avec ++it).

  • Vous pouvez modifier trivialement l'algorithme dans la solution lambda. Par exemple, vous pouvez créer un algorithme qui visite chaque nième élément. Dans de nombreux cas, vous ne vouliez pas vraiment une boucle for, mais un algorithme différent comme copy_if. En utilisant un algorithme + lambda, il est souvent plus facile de changer et est un peu plus concis.

  • D'un autre côté, les programmeurs sont beaucoup plus habitués aux boucles traditionnelles et peuvent donc trouver plus difficile de lire algorithme + lambda.

4
Adrian McCarthy

Premièrement, je ne vois pas beaucoup de différence entre ces deux types, car for_each est implémenté à l'aide de for loop. Mais notez que for_each est une fonction qui a une valeur de retour.

Deuxièmement, je vais utiliser la syntaxe Range Loop une fois disponible dans ce cas, car ce jour viendrait de toute façon bientôt.

0
haohaolee

Effectivement; dans le cas d'utilisation d'une expression Lambda, vous devez déclarer le type et le nom du paramètre afin que rien ne soit gagné.

Mais ce sera génial dès que vous voudrez appeler une fonction (nommée) ou un objet fonction avec cela. (N'oubliez pas que vous pouvez combiner des éléments fonctionnels via std::bind.)

Les livres de Scott Meyers (je crois que c’était Effective STL ) décrivent de tels styles de programmation très bien et clairement.

0
comonad