Veraltet die bereichsbasierte 'for'-Schleife viele einfache Algorithmen?

81

Algorithmuslösung:

std::generate(numbers.begin(), numbers.end(), rand);

Bereichsbasierte For-Loop-Lösung:

for (int& x : numbers) x = rand();

Warum sollte ich std::generatein C ++ 11 die ausführlicheren for-range-basierten for-Schleifen verwenden wollen?

Fredoverflow
quelle
14
Zusammensetzbarkeit? Oh egal, Algorithmen mit Iteratoren sind oft sowieso nicht zusammensetzbar ... :(
R. Martinho Fernandes
2
... was nicht begin()und end()?
the_mandrill
6
@jrok Ich erwarte, dass viele Leute inzwischen eine rangeFunktion in ihrer Toolbox haben. (dh for(auto& x : range(first, last)))
R. Martinho Fernandes
14
boost::generate(numbers, rand); // ♪
Xeo
5
@ JamesBrock Wir haben dies oft im C ++ - Chatraum besprochen (es sollte irgendwo in den Transkripten sein: P). Das Hauptproblem ist, dass Algorithmen häufig einen Iterator zurückgeben und zwei Iteratoren verwenden.
R. Martinho Fernandes

Antworten:

78

Die erste Version

std::generate(numbers.begin(), numbers.end(), rand);

teilt uns mit, dass Sie eine Folge von Werten generieren möchten.

In der zweiten Version muss der Leser das selbst herausfinden.

Das Speichern beim Tippen ist normalerweise nicht optimal, da es meistens in der Lesezeit verloren geht. Der meiste Code wird viel mehr gelesen als eingegeben.

Bo Persson
quelle
13
Sparen beim Tippen? Oh ich verstehe. Warum, oh, warum haben wir den gleichen Begriff für "Sanity Checks zur Kompilierungszeit" und "Tasten auf einer Tastatur drücken"? :)
Fredoverflow
25
"Das Tippen beim Tippen ist normalerweise nicht optimal " Unsinn; Es geht nur darum, welche Bibliothek Sie verwenden. std :: generate ist lang, da Sie numbersohne Grund zweimal angeben müssen . Daher : boost::range::generate(numbers, rand);. Es gibt keinen Grund, warum Sie in einer gut aufgebauten Bibliothek nicht sowohl kürzeren als auch besser lesbaren Code haben können.
Nicol Bolas
9
Es liegt alles im Auge des Lesers. Die Schleifenversion ist mit den meisten Programmierhintergründen verständlich: Setzen Sie jedem Element der Sammlung einen Rand-Wert. Für Std :: generate müssen Sie das aktuelle C ++ kennen oder davon ausgehen, dass "generieren" tatsächlich "Elemente ändern" und nicht "generierte Werte zurückgeben" bedeutet.
Hyde
2
Wenn Sie nur einen Teil eines Containers ändern möchten, können Sie das std::generate(number.begin(), numbers.begin()+3, rand), nicht wahr? Daher numberkann es manchmal nützlich sein , zweimal anzugeben .
Marson Mao
7
@MarsonMao: Wenn Sie nur zwei Argumente haben std::generate(), können Sie stattdessen std::generate(slice(number.begin(), 3), rand)eine hypothetische Range-Slicing-Syntax verwenden std::generate(number[0:3], rand), die die Wiederholung von entfernt und numbergleichzeitig eine flexible Spezifikation eines Teils des Bereichs ermöglicht. Das Umgekehrte ausgehend von drei Argumenten std::generate()ist mühsamer.
Lie Ryan
42

Ob die for-Schleife bereichsbasiert ist oder nicht, macht überhaupt keinen Unterschied, sie vereinfacht nur den Code in der Klammer. Algorithmen sind klarer, da sie die Absicht zeigen .

David Rodríguez - Dribeas
quelle
30

Persönlich meine erste Lektüre von:

std::generate(numbers.begin(), numbers.end(), rand);

ist "Wir weisen alles in einem Bereich zu. Der Bereich ist numbers weisen . Die zugewiesenen Werte sind zufällig".

Meine erste Lektüre von:

for (int& x : numbers) x = rand();

ist "wir machen etwas mit allem in einem Bereich. Der Bereich ist numbers . Wir weisen einen zufälligen Wert zu."

Die sind verdammt ähnlich, aber nicht identisch. Ein plausibler Grund, warum ich die erste Lesung provozieren möchte, ist, dass ich denke, dass die wichtigste Tatsache an diesem Code ist, dass er dem Bereich zugeordnet ist. Also gibt es dein "Warum sollte ich ...". Ich benutze, generateweil in C ++ std::generate"Bereichszuweisung" bedeutet. Wie übrigensstd::copy ist der Unterschied zwischen den beiden, was Sie zuweisen.

Es gibt jedoch verwirrende Faktoren. Bereichsbasierte for-Schleifen haben eine von Natur aus direktere Möglichkeit, den Bereich auszudrücken numbers, als iteratorbasierte Algorithmen. Deshalb arbeiten die Leute an bereichsbasierten Algorithmusbibliotheken: boost::range::generate(numbers, rand);Sieht besser aus als diestd::generate Version.

Im Gegensatz dazu ist int&in Ihrer bereichsbasierten for-Schleife eine Falte. Was ist, wenn der Werttyp des Bereichs nicht der Fall ist? intDann machen wir hier etwas ärgerlich Feines, das davon abhängt, ob er konvertierbar ist int&, während der generateCode nur von der Rückkehr abhängt, randwenn er dem Element zugewiesen werden kann. Selbst wenn der Werttyp ist int, könnte ich immer noch darüber nachdenken, ob dies der Fall ist oder nicht. Daher autowird das Nachdenken über die Typen verschoben, bis ich sehe, was zugewiesen wird - mit der auto &xAussage "Nehmen Sie einen Verweis auf das Bereichselement, welchen Typ auch immer". Zurück in C ++ 03 waren Algorithmen (weil sie Funktionsvorlagen sind) der Weg, um exakte Typen zu verbergen, jetzt sind sie ein Weg.

Ich denke, es war schon immer so, dass die einfachsten Algorithmen nur einen geringfügigen Vorteil gegenüber den äquivalenten Schleifen haben. Range-based for-Schleifen verbessern Schleifen (hauptsächlich durch Entfernen des größten Teils der Boilerplate, obwohl sie etwas mehr enthalten). Die Ränder werden also enger und vielleicht ändern Sie Ihre Meinung in bestimmten Fällen. Aber da gibt es immer noch einen Stilunterschied.

Steve Jessop
quelle
Haben Sie jemals einen benutzerdefinierten Typ mit einem gesehen operator int&()? :)
Fredoverflow
@FredOverflow ersetzen int&durch SomeClass&und jetzt müssen Sie sich um Konvertierungsoperatoren und Einzelparameter-Konstruktoren kümmern, die nicht markiert sind explicit.
TemplateRex
@FredOverflow: Glaube nicht. Deshalb werde ich es nicht erwarten, wenn es jemals passiert, und egal wie paranoid ich jetzt darüber bin, es wird mich beißen, wenn ich dann nicht daran denke ;-) Ein Proxy-Objekt könnte es arbeiten durch Überladen operator int&()und operator int const &() const, aber andererseits könnte es durch Überladen funktionieren operator int() constund operator=(int).
Steve Jessop
1
@rhalbersma: Ich glaube nicht, dass Sie sich um Konstruktoren sorgen müssen, da ein Nicht-Konstanten-Ref nicht an einen temporären gebunden ist. Es sind nur Konvertierungsoperatoren zu Referenztypen.
Steve Jessop
23

Meiner Meinung nach Effective STL Item 43: "Bevorzugen Sie Algorithmusaufrufe gegenüber handgeschriebenen Schleifen." ist immer noch ein guter Rat.

Normalerweise schreibe ich Wrapper-Funktionen, um die begin()/ end()Hölle loszuwerden . Wenn Sie das tun, sieht Ihr Beispiel folgendermaßen aus:

my_util::generate(numbers, rand);

Ich glaube, es übertrifft den auf Schleifen basierenden Bereich sowohl bei der Kommunikation der Absicht als auch bei der Lesbarkeit.


Trotzdem muss ich zugeben, dass in C ++ 98 einige STL-Algorithmusaufrufe unaussprechlichen Code ergaben und das Befolgen von "Algorithmusaufrufe gegenüber handgeschriebenen Schleifen bevorzugen" keine gute Idee zu sein schien. Zum Glück haben Lambdas das geändert.

Betrachten Sie das folgende Beispiel von Herb Sutter: Lambdas, Lambdas Everywhere .

Aufgabe: Finden Sie das erste Element in v, das > xund ist < y.

Ohne Lambdas:

auto i = find_if( v.begin(), v.end(),
bind( logical_and<bool>(),
bind(greater<int>(), _1, x),
bind(less<int>(), _1, y) ) );

Mit Lambda

auto i=find_if( v.begin(), v.end(), [=](int i) { return i > x && i < y; } );
Ali
quelle
1
Ein bisschen orthogonal zur Frage. Nur der erste Satz befasst sich mit der Frage.
David Rodríguez - Dribeas
@ DavidRodríguez-dribeas Ja. Die zweite Hälfte erklärt, warum ich denke, dass Punkt 43 immer noch ein guter Rat ist.
Ali
Mit Boost.Lambda ist es sogar noch besser als mit C ++ - Lambda-Funktionen: auto i = find_if (v.begin (), v.end (), _1> x && _1 <y);
Lego
1
+1 für Wrapper. Dasselbe tun. Sollte von Tag 1 (oder vielleicht 2 ...) im Standard gewesen sein
Macke
22

In meiner Meinung nach ist die manuelle Schleife, obwohl Ausführlichkeit reduzieren könnte, fehlt readabitly:

for (int& x : numbers) x = rand();

Ich würde nicht diese Schleife verwenden zu initialisieren 1 durch definierte der Bereich Zahlen , denn wenn ich es sehe, scheint es mir , dass es Iterieren über eine Reihe von Zahlen, aber es funktioniert nicht (im Wesentlichen) in der Wirklichkeit, das heißt statt Lesen aus dem Bereich, es schreibt auf den Bereich.

Die Absicht ist viel klarer, wenn Sie verwenden std::generate .

1. Initialisieren bedeutet in diesem Zusammenhang , den Elementen des Containers einen sinnvollen Wert zu geben .

Nawaz
quelle
5
Ist das nicht nur so, weil Sie es nicht gewohnt sind, für Schleifen bereichsbasiert zu sein? Mir scheint ziemlich klar, dass diese Anweisung jedem Element im Bereich zugeordnet ist. Es ist klar, dass generate dasselbe tut, wenn Sie damit vertraut sind std::generate, was von einem C ++ - Programmierer angenommen werden kann (wenn sie nicht vertraut sind, werden sie es nachschlagen, dasselbe Ergebnis).
Steve Jessop
4
@SteveJessop: Diese Antwort unterscheidet sich nicht von den beiden anderen. Es erfordert etwas mehr Aufwand vom Leser und ist etwas fehleranfälliger (was ist, wenn Sie ein einzelnes &Zeichen vergessen ?). Der Vorteil von Algorithmen besteht darin, dass sie Absichten zeigen, während Sie bei Schleifen darauf schließen müssen. Wenn bei der Implementierung der Schleife ein Fehler vorliegt, ist nicht klar, ob es sich um einen Fehler handelt oder ob dies beabsichtigt war.
David Rodríguez - Dribeas
1
@ DavidRodríguez-dribeas: Diese Antwort unterscheidet sich von den beiden anderen, IMO erheblich. Es wird versucht herauszufinden, warum der Autor einen Code klarer / verständlicher findet als den anderen. Die anderen sagen es ohne Analyse. Deshalb finde ich dieses interessant genug, um darauf zu antworten :-)
Steve Jessop
1
@SteveJessop: Sie müssen in den Körper der Schleife schauen, um zu dem Schluss zu kommen, dass Sie tatsächlich Zahlen generieren , aber im Falle std::generateeines bloßen Blicks kann man sagen , dass durch diese Funktion etwas generiert wird. Was das ist, wird durch das dritte Argument auf die Funktion beantwortet. Ich denke das ist viel besser.
Nawaz
1
@SteveJessop: Das bedeutet also, dass Sie zur Minderheit gehören. Ich würde Code schreiben, der für die Mehrheit klarer ist: P. Nur das letzte: Ich habe nirgendwo gesagt, dass andere die Schleife genauso lesen werden wie ich. Ich sagte (eher gemeint ), dass dies eine Möglichkeit ist, die Schleife zu lesen, die für mich irreführend ist, und weil der Schleifenkörper vorhanden ist, werden verschiedene Programmierer ihn unterschiedlich lesen, um herauszufinden, was dort geschieht. Sie könnten aus verschiedenen Gründen Einwände gegen die Verwendung einer solchen Schleife erheben, die alle entsprechend ihrer Wahrnehmung korrekt sein könnten.
Nawaz
9

Es gibt einige Dinge, die Sie (einfach) nicht mit bereichsbasierten Schleifen tun können, die Algorithmen, die Iteratoren als Eingabe verwenden, können. Zum Beispiel mit std::generate:

Füllen Sie den Container bis limit(ausgeschlossen, limitist ein gültiger Iterator aktiviert numbers) mit Variablen aus einer Distribution und den Rest mit Variablen aus einer anderen Distribution.

std::generate(numbers.begin(), limit, rand1);
std::generate(limit, numbers.end(), rand2);

Mit Iterator-basierten Algorithmen können Sie den Bereich, in dem Sie arbeiten, besser steuern.

Khaur
quelle
8
Während der Grund für die Lesbarkeit ein RIESIGER Grund ist , Algorithmen zu bevorzugen, ist dies die einzige Antwort, die zeigt, dass die entfernungsbasierte for-Schleife nur eine Teilmenge der Algorithmen ist und daher nichts verwerfen kann ...
K-ballo
6

Für den speziellen Fall von std::generatestimme ich den vorherigen Antworten zum Thema Lesbarkeit / Absicht zu. std :: generate scheint mir eine klarere Version zu sein. Aber ich gebe zu, dass dies in gewisser Weise Geschmackssache ist.

Trotzdem habe ich einen weiteren Grund, den std :: -Algorithmus nicht wegzuwerfen - es gibt bestimmte Algorithmen, die auf einige Datentypen spezialisiert sind.

Das einfachste Beispiel wäre std::fill. Die allgemeine Version wird als for-Schleife über den bereitgestellten Bereich implementiert. Diese Version wird beim Instanziieren der Vorlage verwendet. Aber nicht immer. ZB wenn Sie ihm einen Bereich zur Verfügung stellen, der a ist std::vector<int>- oft wird er tatsächlich anrufenmemset unter der Haube , was einen viel schnelleren und besseren Code ergibt.

Also versuche ich hier eine Effizienzkarte zu spielen.

Ihre handgeschriebene Schleife ist möglicherweise so schnell wie eine std :: algorithm-Version, kann jedoch kaum schneller sein. Darüber hinaus ist der std :: -Algorithmus möglicherweise auf bestimmte Container und Typen spezialisiert und erfolgt über die saubere STL-Schnittstelle.

Sergei Nosov
quelle
3

Meine Antwort wäre vielleicht und nein. Wenn wir über C ++ 11 sprechen, dann vielleicht (eher wie nein). Zum Beispiel std::for_eachist es wirklich ärgerlich, es auch mit Lambdas zu benutzen:

std::for_each(c.begin(), c.end(), [&](ExactTypeOfContainedValue& x)
{
    // do stuff with x
});

Die Verwendung von Range-Based für ist jedoch viel besser:

for (auto& x : c)
{
    // do stuff with x
}

Auf der anderen Seite, wenn wir über C ++ 1y sprechen, würde ich argumentieren, dass nein, die Algorithmen nicht durch bereichsbasierte für überholt werden. Im C ++ - Standardkomitee gibt es eine Studiengruppe, die an einem Vorschlag arbeitet, Bereiche zu C ++ hinzuzufügen, und es wird auch an polymorphen Lambdas gearbeitet. Bereiche würden die Notwendigkeit der Verwendung von Iteratorpaaren beseitigen, und polymorphes Lambda würde es Ihnen ermöglichen, den genauen Argumenttyp des Lambda nicht anzugeben. Dies bedeutet, dass std::for_eachdies so verwendet werden könnte (nehmen Sie dies nicht als harte Tatsache, es ist genau so, wie die Träume heute aussehen):

std::for_each(c.range(), [](x)
{
    // do stuff with x
});
Lego
quelle
Im letzteren Fall besteht der Vorteil des Algorithmus darin, dass Sie beim Schreiben []mit dem Lambda die Nullerfassung angeben. Das heißt, im Vergleich zum Schreiben eines Schleifenkörpers haben Sie einen Teil des Codes aus dem Kontext der variablen Suche isoliert, in dem er lexikalisch angezeigt wird. Die Isolierung ist für den Leser normalerweise hilfreich, weniger zum Nachdenken beim Lesen.
Steve Jessop
1
Die Erfassung ist nicht der Punkt. Der Punkt ist, dass Sie mit polymorphem Lambda nicht explizit angeben müssen, um welchen Typ von x es sich handelt.
Lego
1
In diesem Fall scheint es mir, dass in diesem hypothetischen C ++ 1y for_eachauch bei Verwendung mit einem Lambda noch sinnlos ist. foreach + Capture Lambda ist derzeit eine ausführliche Methode zum Schreiben einer bereichsbasierten for-Schleife. Sie wird etwas weniger ausführlich, aber immer noch mehr als die Schleife. for_eachNatürlich denke ich nicht, dass Sie sich verteidigen müssen, aber noch bevor ich Ihre Antwort sah, dachte ich, wenn der Fragesteller Algorithmen verprügeln wollte, hätte er for_eachdas weichste aller möglichen Ziele auswählen können ;-)
Steve Jessop
Nicht zu verteidigen for_each, aber es hat einen winzigen Vorteil gegenüber bereichsbasiert für - Sie können es einfacher parallel machen, indem Sie ihm parallel_ voranstellen, um es zu verwandeln parallel_for_each(wenn Sie PPL verwenden und davon ausgehen, dass es threadsicher ist). . :-D
lego
@lego Ihr "winziger" Vorteil ist in der Tat ein "großer" Vorteil, wenn Sie ihn so verallgemeinern, dass die std::algorithmImplementierung des s hinter seiner Schnittstelle verborgen ist und beliebig komplex (oder beliebig optimiert) sein kann.
Christian Rau
1

Eine Sache, die beachtet werden sollte, ist, dass ein Algorithmus ausdrückt, was getan wird, nicht wie.

Die bereichsbasierte Schleife umfasst die Art und Weise, wie Dinge erledigt werden: Beginnen Sie mit dem ersten, wenden Sie an und gehen Sie bis zum Ende zum nächsten Element. Sogar ein einfacher Algorithmus könnte die Dinge anders machen (zumindest einige Überladungen für bestimmte Container, ohne an den schrecklichen Vektor zu denken), und zumindest ist die Art und Weise, wie dies gemacht wird, nicht das Geschäft der Autoren.

Für mich ist das ein großer Unterschied, kapseln Sie so viel wie möglich ein, und das rechtfertigt den Satz, wenn Sie können, verwenden Sie Algorithmen.

Cedric
quelle
1

Eine bereichsbasierte for-Schleife ist genau das. Bis natürlich Standard geändert wird.

Algorithmus ist eine Funktion. Eine Funktion, die einige Anforderungen an ihre Parameter stellt. Die Anforderungen sind in einem Standard formuliert, um beispielsweise eine Implementierung zu ermöglichen, die alle verfügbaren Ausführungsthreads nutzt und Sie automatisch beschleunigt.

Tomek
quelle