Warum definiert der Standard end()
als eins nach dem Ende anstatt am tatsächlichen Ende?
Warum definiert der Standard end()
als eins nach dem Ende anstatt am tatsächlichen Ende?
Antworten:
Das beste Argument ist das von Dijkstra selbst :
Sie wollen , dass die Größe des Bereichs ein einfaches Unterschied zu Ende - beginnen ;
Das Einschließen der Untergrenze ist "natürlicher", wenn Sequenzen zu leeren degenerieren, und auch, weil die Alternative (mit Ausnahme der Untergrenze) die Existenz eines "Eins-vor-dem-Anfang" -Sentinelwerts erfordern würde.
Sie müssen immer noch begründen, warum Sie anfangen, bei Null anstatt bei Eins zu zählen, aber das war nicht Teil Ihrer Frage.
Die Weisheit hinter der Konvention [Anfang, Ende] zahlt sich immer wieder aus, wenn Sie einen Algorithmus haben, der sich mit mehreren verschachtelten oder iterierten Aufrufen von bereichsbasierten Konstruktionen befasst, die sich auf natürliche Weise verketten. Im Gegensatz dazu würde die Verwendung eines doppelt geschlossenen Bereichs zu einzelnen und äußerst unangenehmen und verrauschten Codes führen. Betrachten Sie zum Beispiel eine Partition [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Ein weiteres Beispiel ist die Standard-Iterationsschleife for (it = begin; it != end; ++it)
, die end - begin
Zeiten ausführt. Der entsprechende Code wäre viel weniger lesbar, wenn beide Enden inklusive wären - und stellen Sie sich vor, wie Sie mit leeren Bereichen umgehen würden.
Schließlich können wir auch ein gutes Argument dafür liefern, warum das Zählen bei Null beginnen sollte: Mit der halboffenen Konvention für Bereiche, die wir gerade festgelegt haben, wenn Sie einen Bereich von N Elementen erhalten (z. B. um die Mitglieder eines Arrays aufzulisten), dann 0 ist der natürliche "Anfang", so dass Sie den Bereich als [0, N ) schreiben können , ohne umständliche Offsets oder Korrekturen.
Kurz gesagt: Die Tatsache, dass wir die Zahl 1
in bereichsbasierten Algorithmen nicht überall sehen, ist eine direkte Folge und Motivation für die Konvention [Anfang, Ende].
begin
und end
als int
s mit Werten 0
bzw. denken N
, passt es perfekt. Es ist wohl der !=
Zustand, der natürlicher ist als der traditionelle <
, aber das haben wir erst entdeckt, als wir über allgemeinere Sammlungen nachdachten.
++
inkrementierbare Iteratorvorlage schreiben step_by<3>
, die dann die ursprünglich angekündigte Semantik aufweist.
!=
wann er verwenden sollte <
, dann ist es ein Fehler. Übrigens ist dieser Fehlerkönig bei Unit-Tests oder Behauptungen leicht zu finden.
Tatsächlich ist eine Menge iteratorbezogener Dinge plötzlich viel sinnvoller, wenn man bedenkt, dass die Iteratoren nicht auf die Elemente der Sequenz zeigen, sondern dazwischen , wobei die Dereferenzierung auf das nächste Element direkt darauf zugreift. Dann macht der Iterator "one past end" plötzlich Sinn:
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
begin end
Zeigt offensichtlich begin
auf den Anfang der Sequenz und end
auf das Ende derselben Sequenz. Die Dereferenzierung begin
greift auf das Element zu A
, und die Dereferenzierung end
macht keinen Sinn, da kein Elementrecht vorhanden ist. Auch das Hinzufügen eines Iterators i
in der Mitte ergibt
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
und Sie sehen sofort, dass der Bereich der Elemente von begin
bis i
die Elemente enthält A
und B
während der Bereich der Elemente von i
bis end
die Elemente C
und enthält D
. Dereferenzierung i
gibt das Element rechts davon, das ist das erste Element der zweiten Sequenz.
Sogar das "Off-by-One" für Reverse-Iteratoren wird auf diese Weise plötzlich offensichtlich: Das Umkehren dieser Sequenz ergibt:
+---+---+---+---+
| D | C | B | A |
+---+---+---+---+
^ ^ ^
| | |
rbegin ri rend
(end) (i) (begin)
Ich habe die entsprechenden nicht-umgekehrten (Basis-) Iteratoren in Klammern unten geschrieben. Sie sehen, der umgekehrte Iterator von i
(den ich benannt habe ri
) zeigt immer noch zwischen den Elementen B
und C
. Aufgrund der Umkehrung der Reihenfolge befindet sich das Element jetzt B
rechts davon.
foo[i]
) ist eine Abkürzung für das Element unmittelbar nach der Position i
). Wenn ich darüber nachdenke, frage ich mich, ob es für eine Sprache nützlich sein könnte, separate Operatoren für "Element unmittelbar nach Position i" und "Element unmittelbar vor Position i" zu haben, da viele Algorithmen mit Paaren benachbarter Elemente arbeiten und sagen: " Die Gegenstände auf beiden Seiten der Position i "können sauberer sein als" Die Gegenstände an den Positionen i und i + 1 ".
begin[0]
(unter der Annahme eines Iterators mit wahlfreiem Zugriff) auf das Element zugegriffen werden 1
, da 0
meine Beispielsequenz kein Element enthält.
start()
in Ihrer Klasse definieren müssen, um einen bestimmten Prozess zu starten oder was auch immer, wäre es ärgerlich, wenn sie mit einem bereits vorhandenen in Konflikt steht).
Warum definiert der Standard end()
als eins nach dem Ende anstatt am tatsächlichen Ende?
Weil:
begin()
gleich
end()
& end()
nicht erreicht werden.Weil dann
size() == end() - begin() // For iterators for whom subtraction is valid
und Sie müssen keine unangenehmen Dinge wie tun
// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }
und Sie werden nicht versehentlich fehlerhaften Code wie schreiben
bool empty() { return begin() == end() - 1; } // a typo from the first version
// of this post
// (see, it really is confusing)
bool empty() { return end() - begin() == -1; } // Signed/unsigned mismatch
// Plus the fact that subtracting is also invalid for many iterators
Außerdem: Was würde find()
zurückkehren, wenn end()
auf ein gültiges Element verwiesen würde ?
Haben Sie wirklich wollen , ein anderes Mitglied genannt , invalid()
die eine ungültige Iterator zurück ?!
Zwei Iteratoren sind schon schmerzhaft genug ...
Oh, und siehe diesen verwandten Beitrag .
Wenn das end
vor dem letzten Element wäre, wie würdest du insert()
am wahren Ende sein?!
Das Iterator-Idiom von halb geschlossenen Bereichen [begin(), end())
basiert ursprünglich auf der Zeigerarithmetik für einfache Arrays. In dieser Betriebsart hätten Sie Funktionen, denen ein Array und eine Größe übergeben wurden.
void func(int* array, size_t size)
Das Konvertieren in halbgeschlossene Bereiche [begin, end)
ist sehr einfach, wenn Sie über folgende Informationen verfügen:
int* begin;
int* end = array + size;
for (int* it = begin; it < end; ++it) { ... }
Es ist schwieriger, mit vollständig geschlossenen Bereichen zu arbeiten:
int* begin;
int* end = array + size - 1;
for (int* it = begin; it <= end; ++it) { ... }
Da Zeiger auf Arrays in C ++ Iteratoren sind (und die Syntax dies ermöglicht), ist das Aufrufen viel einfacher std::find(array, array + size, some_value)
als das Aufrufen std::find(array, array + size - 1, some_value)
.
Plus, wenn Sie mit halbgeschlossenen Bereichen arbeiten, können Sie die verwenden können !=
Betreiber für den End-Zustand zu überprüfen, becuase (wenn Ihr Betreiber korrekt definiert ist) <
bedeutet !=
.
for (int* it = begin; it != end; ++ it) { ... }
Bei vollständig geschlossenen Bereichen ist dies jedoch nicht einfach. Du steckst fest <=
.
Die einzige Art von Iterator, die C ++ unterstützt <
und >
Operationen ausführt, sind Iteratoren mit wahlfreiem Zugriff. Wenn Sie <=
für jede Iteratorklasse in C ++ einen Operator schreiben müssten, müssten Sie alle Ihre Iteratoren vollständig vergleichbar machen, und Sie hätten weniger Auswahlmöglichkeiten für die Erstellung weniger leistungsfähiger Iteratoren (z. B. der bidirektionalen Iteratoren std::list
oder der Eingabeiteratoren) die funktionieren iostreams
), wenn C ++ vollständig geschlossene Bereiche verwendet.
Wenn der end()
Zeiger nach dem Ende zeigt, ist es einfach, eine Sammlung mit einer for-Schleife zu iterieren:
for (iterator it = collection.begin(); it != collection.end(); it++)
{
DoStuff(*it);
}
Mit end()
Hinweis auf das letzte Element, würde eine Schleife komplexe:
iterator it = collection.begin();
while (!collection.empty())
{
DoStuff(*it);
if (it == collection.end())
break;
it++;
}
begin() == end()
.!=
anstelle von <
(weniger als) zu verwenden, daher end()
ist es praktisch , auf eine Position außerhalb des Endes zu zeigen.