Почему стандартные диапазоны итераторов [начало, конец) вместо [начало, конец]?


204

Почему Стандарт определяется end()как конец за концом, а не как фактический конец?


19
Я предполагаю, "потому что это то, что говорит стандарт", не будет сокращать это, правильно? :)
Лучиан Григоре

39
@LuchianGrigore: Конечно нет. Это подорвало бы наше уважение к (людям, стоящим за) стандартом. Мы должны ожидать, что есть причина для выбора, сделанного стандартом.
Kerrek SB

4
Короче говоря, компьютеры не считаются людьми. Но если вам интересно, почему люди не считают компьютеры, я рекомендую «Ничего, что есть: естественная история нуля», чтобы глубже взглянуть на проблему, с которой столкнулись люди, обнаружив, что число на единицу меньше чем один.
Джон Макфарлейн

8
Поскольку есть только один способ генерировать «последний», он часто не дешевый, потому что он должен быть реальным. Генерирование «ты упал с обрыва» всегда дешево, подойдут многие возможные представления. (пусто *) "ааааа" подойдет.
Ганс Пассант

6
Я посмотрел на дату вопроса и на секунду подумал, что ты шутишь.
Асаф

Ответы:


286

Лучшим аргументом легко является тот, который выдвинул сам Дейкстра :

  • Вы хотите, чтобы размер диапазона , чтобы быть простой разницей конца  -  начать ;

  • включение нижней границы является более «естественным», когда последовательности вырождаются в пустые, а также потому, что альтернатива ( исключая нижнюю границу) потребует существования значения «один перед началом» дозорного.

Вам все еще нужно объяснить, почему вы начинаете считать с нуля, а не с одного, но это не было частью вашего вопроса.

Мудрость, лежащая в основе соглашения [начало, конец), окупается снова и снова, когда у вас есть какой-либо алгоритм, который имеет дело с несколькими вложенными или повторяющимися вызовами конструкций на основе диапазона, которые естественным образом объединяются. Напротив, использование дважды закрытого диапазона повлечет за собой посторонний и крайне неприятный и шумный код. Например, рассмотрим раздел [ n 0 , n 1 ) [ n 1 , n 2 ) [ n 2 , n 3 ). Другим примером является стандартный цикл итерации for (it = begin; it != end; ++it), который запускается end - beginраз. Соответствующий код был бы намного менее читабельным, если бы оба конца были включительно - и представьте, как вы будете обрабатывать пустые диапазоны.

Наконец, мы также можем привести хороший аргумент, почему подсчет должен начинаться с нуля: с полуоткрытым соглашением для только что установленных диапазонов, если вам дан диапазон из N элементов (скажем, для перечисления членов массива), тогда 0 является естественным «началом», так что вы можете записать диапазон как [0, N ), без каких-либо неловких смещений или исправлений.

В двух словах: тот факт, что мы не видим число 1повсюду в алгоритмах на основе диапазонов, является прямым следствием и [мотивацией] соглашения [начало, конец).


2
Типичный цикл C для итерации по массиву размера N - это «for (i = 0; i <N; i ++) a [i] = 0;». Теперь вы не можете выразить это напрямую с помощью итераторов - многие люди теряют время, пытаясь придать значимость. Но почти в равной степени очевидно, что "for (i = 0; i! = N; i ++) ..." Отображение 0 для начала и N для конца поэтому удобно.
Крейзи Глеу

3
@KrazyGlew: я не помещал типы в пример цикла специально. Если вы думаете, beginи endкак ints со значениями 0и N, соответственно, он идеально подходит. Возможно, это !=условие более естественное, чем традиционное <, но мы никогда не открывали его, пока не начали думать о более общих коллекциях.
Kerrek SB

4
@KerrekSB: Я согласен с тем, что «мы никогда не открывали это [! = Лучше], пока не начали думать о более общих коллекциях». ИМХО, это одна из вещей, за которые Степанов заслуживает похвалы - выступая как человек, который пытался написать такие библиотеки шаблонов до STL. Тем не менее, я буду спорить о том, что «! =» Является более естественным - или, скорее, я буду утверждать, что! =, Вероятно, ввел ошибки, которые <поймают. Подумайте (i = 0; i! = 100; i + = 3) ...
Крейзи Глью

@KrazyGlew: Ваш последний пункт несколько не по теме, поскольку последовательность {0, 3, 6, ..., 99} не имеет формы, о которой спрашивал ОП. Если вы хотите, чтобы это было так, вы должны написать ++-крименабельный шаблон итератора step_by<3>, который будет иметь первоначально объявленную семантику.
Kerrek SB

@KrazyGlew Даже если <когда-нибудь спрятает ошибку, это все равно ошибка . Если кто-то использует, !=когда он должен использовать <, то это ошибка. Кстати, этот король ошибки легко найти с помощью модульного тестирования или утверждений.
Phil1970

80

На самом деле, многие вещи, связанные с итераторами, внезапно приобретают гораздо больший смысл, если учесть, что итераторы указывают не на элементы последовательности, а между ними , с разыменованием доступа к следующему элементу прямо к нему. Тогда итератор «один конец» внезапно приобретает смысл:

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^               ^
   |               |
 begin            end

Очевидно, beginуказывает на начало последовательности и endуказывает на конец той же последовательности. Разыменование beginобращается к элементу A, и разыменование endне имеет смысла, потому что нет никакого элемента к нему. Кроме того, добавление итератора iв середине дает

   +---+---+---+---+
   | A | B | C | D |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
 begin     i      end

и вы сразу видите, что диапазон элементов от beginдо iсодержит элементы, Aа Bдиапазон элементов от iдо endсодержит элементы Cи D. Разыменование iдает право на элемент, то есть первый элемент второй последовательности.

Даже «отключенный» для обратных итераторов внезапно становится очевидным, если поменять эту последовательность:

   +---+---+---+---+
   | D | C | B | A |
   +---+---+---+---+
   ^       ^       ^
   |       |       |
rbegin     ri     rend
 (end)    (i)   (begin)

Я написал соответствующие не обратные (базовые) итераторы в скобках ниже. Видите ли, обратный итератор, принадлежащий i(который я назвал ri), все еще указывает между элементами Bи C. Однако из-за изменения последовательности теперь элемент Bнаходится справа от него.


2
Это ИМХО лучший ответ, хотя я думаю, что он мог бы быть лучше проиллюстрирован, если итераторы указывали на числа, а элементы располагались между числами (синтаксис foo[i]) является сокращением для элемента сразу после позиции i). Размышляя об этом, я задаюсь вопросом: может быть полезно для языка иметь отдельные операторы для «элемента сразу после позиции i» и «элемента непосредственно перед позицией i», поскольку многие алгоритмы работают с парами смежных элементов и говорят « Предметы по обе стороны от позиции i "могут быть чище, чем" Предметы в позициях i и i + 1 ".
суперкат

@supercat: числа должны были указывать позиции и индексы итератора, а указывать сами элементы. Я заменю цифры буквами, чтобы сделать это понятнее. Действительно, при заданных числах begin[0](при условии, что итератор произвольного доступа) будет обращаться к элементу 1, поскольку 0в моем примере последовательности нет элемента .
celtschk

Почему используется слово «начало», а не «начало»? В конце концов, «начать» это глагол.
user1741137

@ user1741137 Я думаю, что «начало» означает аббревиатуру «начало» (что теперь имеет смысл). «начало» слишком длинное, «начало» звучит как хорошая подгонка. «start» будет конфликтовать с глаголом «start» (например, когда вам нужно определить функцию start()в вашем классе для запуска определенного процесса или чего-то еще, это будет раздражать, если оно конфликтует с уже существующим).
Фаэнора

74

Почему Стандарт определяется end()как конец за концом, а не как фактический конец?

Так как:

  1. Это позволяет избежать специальной обработки пустых диапазонов. Для пустых диапазонов begin()равно end()&
  2. Это делает конечный критерий простым для циклов, которые перебирают элементы: циклы просто продолжаются до тех пор, end()пока они не достигнуты.

64

Потому что тогда

size() == end() - begin()   // For iterators for whom subtraction is valid

и вам не придется делать неловкие вещи, как

// Never mind that this is INVALID for input iterators...
bool empty() { return begin() == end() + 1; }

и вы не будете случайно писать ошибочный код, такой как

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

Кроме того: Что будет find()возвращаться, если end()указан правильный элемент?
Вы действительно хотите другого члена с именем, invalid()который возвращает неверный итератор ?!
Двух итераторов уже достаточно больно ...

О, и посмотрите этот пост .


Также:

Если бы это endбыло до последнего элемента, как бы вы insert()в истинном конце ?!


2
Это очень недооцененный ответ. Примеры являются краткими и прямыми, а слова «также» не были сказаны кем-либо еще и являются вещами, которые кажутся очень очевидными в ретроспективе, но поражают меня как откровения.
underscore_d

@underscore_d: Спасибо! :)
user541686

Кстати, в случае, если я выгляжу как лицемер за то, что не проголосовал, это потому, что я уже сделал это еще в июле 2016 года!
underscore_d

@underscore_d: хахаха, я даже не заметил, но спасибо! :)
user541686

22

Идиома итератора полузакрытых диапазонов [begin(), end())изначально основана на арифметике указателей для простых массивов. В этом режиме работы у вас будут функции, которым передан массив и размер.

void func(int* array, size_t size)

Преобразование в полузакрытые диапазоны [begin, end)очень просто, если у вас есть эта информация:

int* begin;
int* end = array + size;

for (int* it = begin; it < end; ++it) { ... }

Работать с полностью закрытыми диапазонами сложнее:

int* begin;
int* end = array + size - 1;

for (int* it = begin; it <= end; ++it) { ... }

Поскольку указатели на массивы являются итераторами в C ++ (и синтаксис был разработан для этого), вызывать их гораздо проще, std::find(array, array + size, some_value)чем вызывать std::find(array, array + size - 1, some_value).


Кроме того, если вы работаете с полузакрытыми диапазонами, вы можете использовать !=оператор для проверки конечного условия, поскольку это <подразумевает (если ваши операторы определены правильно) !=.

for (int* it = begin; it != end; ++ it) { ... }

Однако нет простого способа сделать это с полностью закрытыми диапазонами. Вы застряли <=.

Единственный вид итератора, который поддерживает <и >работает в C ++, - это итераторы с произвольным доступом. Если бы вам нужно было написать <=оператор для каждого класса итераторов в C ++, вам нужно было бы сделать все ваши итераторы полностью сопоставимыми, и у вас было бы меньше вариантов для создания менее способных итераторов (таких как двунаправленные итераторы std::listили входные итераторы). которые работают iostreams), если C ++ использовал полностью закрытые диапазоны.


8

С end()указанием одного за концом легко выполнить итерацию коллекции с помощью цикла for:

for (iterator it = collection.begin(); it != collection.end(); it++)
{
    DoStuff(*it);
}

С end()указанием на последний элемент цикл будет более сложным:

iterator it = collection.begin();
while (!collection.empty())
{
    DoStuff(*it);

    if (it == collection.end())
        break;

    it++;
}

0
  1. Если контейнер пуст, begin() == end().
  2. Программисты на C ++ склонны использовать !=вместо <(меньше, чем) в условиях цикла, поэтому end()удобно указывать на позицию в начале.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.