Bevorzugen Sie Algorithmen gegenüber handgeschriebenen Schleifen?

10

Welche der folgenden Aussagen finden Sie besser lesbar? Die handgeschriebene Schleife:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Oder der Algorithmusaufruf:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Ich frage mich, ob std::for_eaches sich wirklich lohnt, wenn man bedenkt, dass ein so einfaches Beispiel bereits so viel Code erfordert.

Was denkst du über diese Angelegenheit?

Fredoverflow
quelle
2
Für C ++ ist die Antwort ziemlich offensichtlich. In anderen Sprachen sind beide ungefähr gleich (z. B. Python: map(bar.process, vec)obwohl von einer Karte für Nebenwirkungen abgeraten wird und Listenverständnisse / Generatorausdrücke gegenüber der Karte empfohlen werden).
5
Und dann gibt es noch BOOST_FOREACH...
Benjamin Bannier
Der Artikel in der effektiven STL hat Sie also nicht überzeugt? stackoverflow.com/questions/135129/…
Job

Antworten:

18

Es gibt einen Grund, warum Lambdas eingeführt wurden, und das liegt daran, dass sogar das Standardkomitee erkennt, dass die zweite Form scheiße ist. Verwenden Sie das erste Formular, bis Sie C ++ 0x- und Lambda-Unterstützung erhalten.

DeadMG
quelle
2
Dieselbe Argumentation kann verwendet werden, um die zweite Form zu rechtfertigen: Der Standardausschuss erkannte an, dass sie so viel überlegen war, dass sie Lambdas einführten, um sie noch besser zu machen. :-p (+1 trotzdem)
Konrad Rudolph
Ich glaube nicht, dass das zweite so schlimm ist. Aber es kann besser sein. Zum Beispiel haben Sie die Verwendung des Bar-Objekts absichtlich erschwert, indem Sie keinen Operator () hinzugefügt haben. Auf diese Weise wird der Aufrufpunkt in der Schleife erheblich vereinfacht und der Code aufgeräumt.
Martin York
Hinweis: Die Lambda-Unterstützung wurde aufgrund von zwei Hauptbeschwerden hinzugefügt. 1) Die Standardkesselplatte, die benötigt wird, um Parameter an Funktoren zu übergeben. 2) Den Code nicht am Anrufpunkt haben. Ihr Code erfüllt keine dieser Anforderungen, da Sie nur eine Methode aufrufen. Also 1) kein zusätzlicher Code zum Übergeben von Parametern 2) Der Code befindet sich bereits nicht am Aufrufpunkt, sodass Lambda die Wartbarkeit des Codes nicht verbessert. Also ja, Lambda ist eine großartige Ergänzung. Dies ist kein gutes Argument für sie.
Martin York
9

Verwenden Sie immer die Variante, die am besten beschreibt, was Sie vorhaben. Das ist

Tun Sie für jedes Element xin .vecbar.process(x)

Lassen Sie uns nun die Beispiele untersuchen:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Wir haben auch eine for_each- yippeh . Wir haben die [begin; end)Reichweite, mit der wir arbeiten wollen.

Im Prinzip war der Algorithmus viel expliziter und daher jeder handschriftlichen Implementierung vorzuziehen. Aber dann ... Bindemittel? Memfun? Grundsätzlich C ++ interna, wie man eine Mitgliedsfunktion bekommt? Für meine Aufgabe interessieren sie mich nicht ! Ich möchte auch nicht unter dieser wortreichen, gruseligen Syntax leiden.

Nun die andere Möglichkeit:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Zugegeben, dies ist ein häufig zu erkennendes Muster, aber ... Iteratoren erstellen, Schleifen erstellen, inkrementieren, dereferenzieren. Auch dies sind alles Dinge, die mir egal sind , um meine Aufgabe zu erledigen.

Zugegeben, es sieht viel besser aus als die erste Lösung (zumindest ist der Loop- Body flexibel und ziemlich explizit), aber es ist nicht wirklich so toll. Wir werden dieses verwenden, wenn wir keine bessere Möglichkeit hätten, aber vielleicht haben wir ...

Ein besserer Weg?

Nun zurück zu for_each. Wäre es nicht großartig, buchstäblich zu sagen for_each und flexibel in der Operation zu sein, die auch durchgeführt werden soll? Glücklicherweise sind wir seit C ++ 0x Lambdas

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Nachdem wir eine abstrakte, generische Lösung für viele verwandte Situationen gefunden haben, ist es erwähnenswert, dass es in diesem speziellen Fall einen absoluten Favoriten Nr. 1 gibt :

foreach(const Foo& x, vec) bar.process(x);

Klarer geht es wirklich nicht. Zum Glück ist in C ++ 0x eine ähnliche Syntax integriert !

Dario
quelle
2
Diese "ähnliche Syntax" wird verwendet for (const Foo& x : vec) bar.process(x);oder verwendet, const auto&wenn Sie möchten.
Jon Purdy
const auto&ist möglich? Wusste das nicht - tolle Infos!
Dario
8

Weil das so unlesbar ist?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}
Martin Beckett
quelle
4
Entschuldigung, aber aus irgendeinem Grund steht dort "zensiert", wo ich glaube, dass Ihr Code sein sollte.
Mateen Ulhaq
6
Ihr Code gibt eine Compiler-Warnung aus, da der Vergleich von a intmit a size_tgefährlich ist. Außerdem funktioniert die Indizierung beispielsweise nicht bei jedem Container std::set.
Fredoverflow
2
Dieser Code funktioniert nur für Container mit einem Indexierungsoperator, von denen es nur wenige gibt. Es bindet den Algorithmus unnötig fest - was wäre, wenn eine Karte <> besser passen würde? Das Original für loop und for_each wäre davon nicht betroffen.
JBRWilkinson
2
Und wie oft entwerfen Sie Code für einen Vektor und entscheiden sich dann, ihn gegen eine Karte auszutauschen? Als ob das Entwerfen dafür die Priorität der Sprachen wäre.
Martin Beckett
2
Das Iterieren über Sammlungen nach Index wird nicht empfohlen, da einige Sammlungen eine schlechte Leistung bei der Indizierung aufweisen, während sich alle Sammlungen bei Verwendung eines Iterators gut verhalten.
Slaphappy
3

Im Allgemeinen ist die erste Form für so ziemlich jeden lesbar, der weiß, was eine for-Schleife ist, unabhängig davon, welchen Hintergrund er hat.

Auch im Allgemeinen ist der zweite überhaupt nicht so lesbar: leicht genug herauszufinden, was for_each tut, aber wenn Sie ihn noch nie gesehen haben, std::bind1st(std::mem_fun_refkann ich mir vorstellen, dass er schwer zu verstehen ist.

stijn
quelle
2

Selbst mit C ++ 0x bin ich mir nicht sicher for_each, ob ich viel Liebe bekommen werde.

for(Foo& foo: vec) { bar.process(foo); }

Ist viel besser lesbar.

Das einzige, was ich an Algorithmen (in C ++) nicht mag, ist, dass sie über Iteratoren argumentieren und sehr ausführliche Aussagen machen.

Matthieu M.
quelle
2

Wenn Sie als Funktor Bar geschrieben hätten, wäre es viel einfacher:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Hier ist der Code gut lesbar.

Martin York
quelle
2
Es ist ziemlich lesbar, abgesehen von der Tatsache, dass Sie gerade einen Funktor geschrieben haben.
Winston Ewert
Sehen Sie hier, warum ich denke, dass dieser Code schlechter ist.
sbi
1

Ich bevorzuge letzteres, weil es ordentlich und sauber ist. Es ist eigentlich Teil vieler anderer Sprachen, aber in C ++ ist es Teil der Bibliothek. Es ist nicht wirklich wichtig.

Sarat
quelle
0

Nun, dieses Problem ist eigentlich nicht spezifisch für C ++, würde ich behaupten.

Die Sache ist, dass das Übergeben eines Funktors in eine weitere Funktion nicht dazu gedacht ist, Schleifen zu ersetzen .
Es soll bestimmte Muster vereinfachen, die mit funktionaler Programmierung sehr natürlich werden, wie Besucher und Beobachter , um nur zwei zu nennen, die mir sofort in den Sinn kommen.
In Sprachen, denen Funktionen erster Ordnung fehlen (Java ist wahrscheinlich das beste Beispiel), erfordern solche Ansätze immer die Implementierung einer bestimmten Schnittstelle, die recht ausführlich und redundant ist.

Eine häufige Verwendung, die ich in anderen Sprachen häufig sehe, wäre:

someCollection.forEach(someUnaryFunctor);

Dies hat den Vorteil, dass Sie nicht wissen müssen, wie someCollectionoder was tatsächlich implementiert someUnaryFunctorwird. Alles, was Sie wissen müssen, ist, dass die forEachMethode alle Elemente der Sammlung iteriert und sie an die angegebene Funktion übergibt.

Wenn Sie in der Lage sind, alle Informationen über die Datenstruktur, über die Sie iterieren möchten, und alle Informationen darüber, was Sie in jedem Iterationsschritt tun möchten, zu haben, ist ein funktionaler Ansatz eine Überkomplikation, insbesondere in einer Sprache. wo dies anscheinend ziemlich langweilig ist.

Beachten Sie auch, dass der funktionale Ansatz langsamer ist, da Sie viele Anrufe haben, die mit bestimmten Kosten verbunden sind und die Sie nicht in einer for-Schleife haben.

back2dos
quelle
1
"Außerdem sollten Sie bedenken, dass der funktionale Ansatz langsamer ist, da Sie viele Anrufe haben, die mit bestimmten Kosten verbunden sind und die Sie nicht in einer for-Schleife haben." ? Diese Aussage wird nicht unterstützt. Der funktionale Ansatz ist nicht unbedingt langsamer, zumal es unter der Haube zu Optimierungen kommen kann. Und selbst wenn Sie Ihre Schleife vollständig verstehen, wird sie in ihrer abstrakteren Form wahrscheinlich sauberer aussehen.
Andres F.
@Andres F: Das sieht also sauberer aus? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));Und es ist entweder zur Laufzeit langsamer oder zur Kompilierungszeit (wenn Sie Glück haben). Ich verstehe nicht, warum das Ersetzen von Flusssteuerungsstrukturen durch Funktionsaufrufe den Code besser macht. Über jede hier vorgestellte Alternative ist besser lesbar.
back2dos
0

Ich denke, das Problem hier ist, dass for_eaches sich nicht wirklich um einen Algorithmus handelt, sondern nur um eine andere (und normalerweise minderwertige) Art, eine handgeschriebene Schleife zu schreiben. Aus dieser Perspektive sollte es der am wenigsten verwendete Standardalgorithmus sein, und ja, Sie könnten wahrscheinlich genauso gut eine for-Schleife verwenden. Der Ratschlag im Titel bleibt jedoch bestehen, da es mehrere Standardalgorithmen gibt, die auf spezifischere Anwendungen zugeschnitten sind.

Wo es einen Algorithmus gibt, der enger macht, was Sie wollen, dann sollten Sie Algorithmen gegenüber handgeschriebenen Schleifen bevorzugen. offensichtlichsten Beispiele sind hier das Sortieren mit sortoder stable_sortoder mit der Suche lower_bound, upper_boundoder equal_range, aber die meisten von ihnen haben eine Situation , in der sie bevorzugt eine Hand codiert Schleife zu verwenden , um über.

jk.
quelle
0

Für dieses kleine Code-Snippet sind beide für mich gleichermaßen lesbar. Im Allgemeinen finde ich manuelle Schleifen fehleranfällig und bevorzuge die Verwendung von Algorithmen, aber es hängt wirklich von einer konkreten Situation ab.

Nemanja Trifunovic
quelle