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

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

7 ответов


лучший аргумент легко это сделал Дейкстра:

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

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

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

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

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

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


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

потому что:

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

на самом деле, много итераторов, связанных с вещами, внезапно имеет гораздо больше смысла, если вы считаете, что итераторы не указывают at элементы последовательности, но между, С разыменованием доступа к следующему элементу справа от него. Тогда итератор" one past end " внезапно имеет непосредственный смысл:

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

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

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

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

даже "off-by-one" для обратных итераторов внезапно становится очевидным таким образом: обращение этой последовательности дает:

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

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


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

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() в настоящий конец?!


идиома итератора полузакрытых диапазонов [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++ использовал полностью закрытые диапазоны.


С 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++;
}

  1. если контейнер пуст, то begin() == end().
  2. программисты C++, как правило, используют != вместо < (меньше чем) в условиях цикла, поэтому имея end() указывать на положение одно-конец удобен.