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?
Ich denke, es gibt einige andere Unterschiede, die in den Antworten bisher noch nicht behandelt wurden.
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.)
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).
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.
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.
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.
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.
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.