Pourquoi la norme définit-elle end()
comme dépassant la fin, plutôt qu'à la fin réelle?
Le meilleur argument est facilement celui avancé par Dijkstra lui-même :
Vous voulez que la taille de la plage soit une simple différence end - begin ;
inclure la borne inférieure est plus "naturel" lorsque les séquences dégénèrent en séquences vides, et aussi parce que l'alternative ( excluant la borne inférieure) nécessiterait l'existence de une valeur sentinelle "un avant le début".
Vous devez toujours justifier pourquoi vous commencez à compter à zéro plutôt qu'à un, mais cela ne faisait pas partie de votre question.
La sagesse derrière la convention [début, fin] est payante à maintes reprises lorsque vous avez une sorte d'algorithme qui traite de multiples appels imbriqués ou itérés vers des constructions basées sur une plage, qui s'enchaînent naturellement. En revanche, l'utilisation d'une plage doublement fermée entraînerait un code décalé et extrêmement désagréable et bruyant. Par exemple, considérons une partition [ n , n 1) [ n 1, n 2) [ n 2, n 3). Un autre exemple est la boucle d'itération standard for (it = begin; it != end; ++it)
, qui exécute end - begin
fois. Le code correspondant serait beaucoup moins lisible si les deux extrémités étaient inclusives - et imaginez comment vous géreriez des plages vides.
Enfin, nous pouvons également faire un argument de Nice pourquoi le comptage devrait commencer à zéro: Avec la convention semi-ouverte pour les plages que nous venons d'établir, si on vous donne une plage de [~ # ~] n [~ # ~] éléments (par exemple pour énumérer les membres d'un tableau), alors 0 est le "début" naturel afin que vous puissiez écrire la plage sous la forme [0, [~ # ~] n [~ # ~] ), sans aucun décalage ou correction gênant.
En bref: le fait que nous ne voyons pas le nombre 1
partout dans les algorithmes basés sur la plage est une conséquence directe et une motivation de la convention [début, fin].
En fait, beaucoup de choses liées aux itérateurs ont soudain beaucoup plus de sens si vous considérez que les itérateurs ne pointent pas at les éléments de la séquence mais entre les deux, avec un déréférencement accédant à l'élément suivant droit à elle. Ensuite, l'itérateur "one past end" prend tout de suite un sens immédiat:
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^
| |
begin end
De toute évidence, begin
pointe vers le début de la séquence et end
pointe vers la fin de la même séquence. Le déréférencement begin
accède à l'élément A
, et le déréférencement end
n'a aucun sens car il n'y a pas d'élément directement. De plus, l'ajout d'un itérateur i
au milieu donne
+---+---+---+---+
| A | B | C | D |
+---+---+---+---+
^ ^ ^
| | |
begin i end
et vous voyez immédiatement que la plage d'éléments de begin
à i
contient les éléments A
et B
tandis que la plage d'éléments de i
à end
contient les éléments C
et D
. Le déréférencement i
donne l'élément droit, c'est-à-dire le premier élément de la deuxième séquence.
Même le "off-by-one" pour les itérateurs inversés devient soudainement évident de cette façon: inverser cette séquence donne:
+---+---+---+---+
| D | C | B | A |
+---+---+---+---+
^ ^ ^
| | |
rbegin ri rend
(end) (i) (begin)
J'ai écrit les itérateurs non inverses (de base) correspondants entre parenthèses ci-dessous. Vous voyez, l'itérateur inversé appartenant à i
(que j'ai nommé ri
) toujours pointe entre les éléments B
et C
. Cependant, en raison de l'inversion de la séquence, l'élément B
est maintenant à droite.
Pourquoi la norme définit-elle end()
comme dépassant la fin, au lieu de la fin réelle?
Car:
begin()
est égal à end()
&end()
n'est pas atteinte.Parce qu'alors
size() == end() - begin() // For iterators for whom subtraction is valid
et vous n'aurez pas à faire des choses maladroites comme
// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }
et vous n'écrirez pas accidentellement du code erroné comme
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
Aussi: Que retournerait find()
si end()
pointait vers un élément valide?
Avez-vous vraiment voulez un autre membre appelé invalid()
qui renvoie un itérateur invalide?!
Deux itérateurs est déjà assez douloureux ...
Oh, et voir this article connexe .
Si le end
était avant le dernier élément, comment feriez-vous insert()
à la vraie fin?!
L'idiome de l'itérateur des plages semi-fermées [begin(), end())
est à l'origine basé sur l'arithmétique des pointeurs pour les tableaux simples. Dans ce mode de fonctionnement, vous auriez des fonctions auxquelles un tableau et une taille ont été transmis.
void func(int* array, size_t size)
La conversion en plages semi-fermées [begin, end)
Est très simple lorsque vous disposez de ces informations:
int* begin;
int* end = array + size;
for (int* it = begin; it < end; ++it) { ... }
Pour travailler avec des gammes entièrement fermées, c'est plus difficile:
int* begin;
int* end = array + size - 1;
for (int* it = begin; it <= end; ++it) { ... }
Comme les pointeurs vers les tableaux sont des itérateurs en C++ (et la syntaxe a été conçue pour permettre cela), il est beaucoup plus facile d'appeler std::find(array, array + size, some_value)
que d'appeler std::find(array, array + size - 1, some_value)
.
De plus, si vous travaillez avec des plages semi-fermées, vous pouvez utiliser l'opérateur !=
Pour vérifier la condition de fin, car (si vos opérateurs sont définis correctement) <
Implique !=
.
for (int* it = begin; it != end; ++ it) { ... }
Cependant, il n'y a pas de moyen facile de le faire avec des plages entièrement fermées. Vous êtes coincé avec <=
.
Le seul type d'itérateur qui prend en charge les opérations <
Et >
En C++ sont les itérateurs à accès aléatoire. Si vous deviez écrire un opérateur <=
Pour chaque classe d'itérateurs en C++, vous auriez à rendre tous vos itérateurs entièrement comparables et vous auriez moins de choix pour créer des itérateurs moins performants (tels que les itérateurs bidirectionnels sur std::list
, ou les itérateurs d'entrée qui fonctionnent sur iostreams
) si C++ utilisait des plages entièrement fermées.
Avec la end()
pointant au-delà de la fin, il est facile d'itérer une collection avec une boucle for:
for (iterator it = collection.begin(); it != collection.end(); it++)
{
DoStuff(*it);
}
Avec end()
pointant vers le dernier élément, une boucle serait plus complexe:
iterator it = collection.begin();
while (!collection.empty())
{
DoStuff(*it);
if (it == collection.end())
break;
it++;
}
begin() == end()
.!=
Au lieu de <
(Moins que) dans des conditions de boucle, donc avoir end()
pointant vers une position à la fin est pratique.