wake-up-neo.com

for loop vs std :: for_each mit Lambda

Betrachten wir eine in C++ 11 geschriebene Template-Funktion, die einen Container durchläuft. Bitte die Range-Loop-Syntax nicht berücksichtigen, da sie von dem Compiler, mit dem ich arbeite, noch nicht unterstützt wird.

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

Was sind Vor-und Nachteile von for-Schleife vs std::for_each in Bezug auf:

eine Leistung? (Ich erwarte keinen Unterschied)

b) Lesbarkeit und Wartbarkeit?

Hier sehe ich viele Nachteile von for_each. Es würde kein Array im c-Stil akzeptieren, während die Schleife dies tun würde. Die Deklaration des Lambda-Formalparameters ist so ausführlich, dass es nicht möglich ist, auto dort zu verwenden. Es ist nicht möglich, for_each auszubrechen.

In 11-Tagen vor C++ waren Argumente gegen for erforderlich, um den Typ für den Iterator anzugeben (nicht mehr gültig) und die einfache Möglichkeit, die Schleifenbedingung falsch einzugeben (ich habe in 10 Jahren keinen solchen Fehler gemacht). .

Als Schlussfolgerung widersprechen meine Gedanken zu for_each der allgemeinen Meinung. Was fehlt mir hier?

36
Andrey

Ich denke, es gibt einige andere Unterschiede, die in den Antworten bisher noch nicht behandelt wurden.

  1. ein for_each kann any - geeignetes aufrufbares Objekt akzeptieren, wodurch der Schleifenrumpf für verschiedene for - Schleifen "recycelt" werden kann. Zum Beispiel (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
    

    wird

    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
    

    dadurch werden Doppelarbeit vermieden und die Codewartung vereinfacht. (Natürlich kann man in einem witzigen Stilmix auch einen ähnlichen Ansatz mit der for-Schleife verwenden.)

  2. ein weiterer Unterschied betrifft das Ausbrechen der Schleife (mit break oder return in der for-Schleife). Soweit ich weiß, kann dies in einer for_each-Schleife nur durch Auslösen einer Ausnahme erfolgen. Zum Beispiel

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

    wird

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

    mit den gleichen Auswirkungen hinsichtlich des Aufrufs von Destruktoren für Objekte mit Umfang des Schleifenkörpers und des Funktionskörpers (um die Schleife).

  3. der Hauptvorteil von for_each ist IMHO, dass es für bestimmte Containertypen überlastet werden kann, wenn die einfache Iteration nicht so effizient ist. Stellen Sie sich beispielsweise einen Container vor, der eine verknüpfte Liste von Datenblöcken enthält, wobei jeder Block ein zusammenhängendes Array von Elementen enthält, ähnlich wie (Fehlen von irrelevantem Code).

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

    ein geeigneter Forward-Iterator für diesen Typ müsste dann bei jedem Inkrement nach dem Blockende suchen, und der Vergleichsoperator muss sowohl den Blockzeiger als auch den Index in jedem Block vergleichen (wobei irrelevanter Code weggelassen wird):

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

    diese Art von Iterator funktioniert beispielsweise korrekt mit for und for_each

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

    erfordert aber zwei bis drei Vergleiche pro Iteration sowie eine Verzweigung. Eine effizientere Methode besteht darin, die Funktion for_each() zu überladen, um den Blockzeiger und den Index separat zu durchlaufen:

    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
    

    dies erfordert nur einen Vergleich für die meisten Iterationen und hat keine Verzweigungen (Beachten Sie, dass Verzweigungen die Performance beeinträchtigen können). Beachten Sie, dass wir dies nicht im Namespace std definieren müssen (was möglicherweise illegal ist), aber Sie können sicherstellen, dass die richtige Version von den entsprechenden using-Anweisungen verwendet wird. Dies ist äquivalent zu using std::swap;, wenn swap() auf bestimmte benutzerdefinierte Typen spezialisiert wird.

32
Walter

In Bezug auf die Leistung ruft Ihre for-Schleife wiederholt std::end auf, während std::for_each dies nicht tut. Dies kann je nach verwendetem Container zu Leistungsunterschieden führen oder nicht.

7
eq-
  • Die std::for_each-Version besucht jedes Element genau einmal. Jemand, der den Code liest, kann dies wissen, sobald er std::for_each sieht, da im Lambda nichts getan werden kann, um den Iterator zu verwirren. In der traditionellen for-Schleife müssen Sie den Körper der Schleife auf ungewöhnlichen Steuerungsfluss untersuchen (continue, break, return) und mit dem Iterator dinking (z. B. überspringen Sie in diesem Fall das nächste Element mit ++it).

  • Sie können den Algorithmus in der Lambda-Lösung trivial ändern. Sie können beispielsweise einen Algorithmus erstellen, der jedes n-te Element besucht. In vielen Fällen wollten Sie sowieso nicht wirklich eine for-Schleife, sondern einen anderen Algorithmus wie copy_if. Die Verwendung eines Algorithmus + Lambda lässt sich oft leichter ändern und ist etwas prägnanter.

  • Auf der anderen Seite sind Programmierer viel gewöhnlicher für traditionelle Loops, so dass Algorithmen + Lambda schwerer zu lesen sind.

4
Adrian McCarthy

Erstens sehe ich keinen großen Unterschied zwischen diesen beiden, da for_each mit for-Schleife implementiert wird. Beachten Sie jedoch, dass for_each eine Funktion ist, die einen Rückgabewert hat.

Zweitens werde ich die in diesem Fall einmal verfügbare Range-Loop-Syntax verwenden, da dieser Tag sowieso bald kommen würde.

0
haohaolee

Tatsächlich; Wenn Sie einen Lambda-Ausdruck verwenden, müssen Sie den Parametertyp und den Namen angeben, damit nichts gewonnen wird.

Es wird jedoch fantastisch sein, sobald Sie eine (benannte) Funktion oder ein Funktionsobjekt damit aufrufen möchten. (Denken Sie daran, dass Sie funktionsähnliche Dinge über std::bind kombinieren können.)

Die Bücher von Scott Meyers (ich glaube es war Effective STL ) beschreiben solche Programmierstile sehr gut und klar.

0
comonad