web-dev-qa-db-fra.com

Pourquoi les plages d'itérateurs standard [début, fin) au lieu de [début, fin]?

Pourquoi la norme définit-elle end() comme dépassant la fin, plutôt qu'à la fin réelle?

198
Puppy

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].

281
Kerrek SB

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.

77
celtschk

Pourquoi la norme définit-elle end() comme dépassant la fin, au lieu de la fin réelle?

Car:

  1. Il évite une manipulation spéciale pour les plages vides. Pour les plages vides, begin() est égal à end() &
  2. Cela rend le critère de fin simple pour les boucles qui itèrent sur les éléments: Les boucles continuent simplement tant que end() n'est pas atteinte.
72
Alok Save

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 .


Également:

Si le end était avant le dernier élément, comment feriez-vous insert() à la vraie fin?!

61
Mehrdad

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.

22
Ken Bloom

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++;
}
8
Anders Abel
  1. Si un conteneur est vide, begin() == end().
  2. Les programmeurs C++ ont tendance à utiliser != Au lieu de < (Moins que) dans des conditions de boucle, donc avoir end() pointant vers une position à la fin est pratique.
0
Andreas DM