Почему Стандарт определяется end()
как конец за концом, а не как фактический конец?
Почему Стандарт определяется end()
как конец за концом, а не как фактический конец?
Ответы:
Лучшим аргументом легко является тот, который выдвинул сам Дейкстра :
Вы хотите, чтобы размер диапазона , чтобы быть простой разницей конца - начать ;
включение нижней границы является более «естественным», когда последовательности вырождаются в пустые, а также потому, что альтернатива ( исключая нижнюю границу) потребует существования значения «один перед началом» дозорного.
Вам все еще нужно объяснить, почему вы начинаете считать с нуля, а не с одного, но это не было частью вашего вопроса.
Мудрость, лежащая в основе соглашения [начало, конец), окупается снова и снова, когда у вас есть какой-либо алгоритм, который имеет дело с несколькими вложенными или повторяющимися вызовами конструкций на основе диапазона, которые естественным образом объединяются. Напротив, использование дважды закрытого диапазона повлечет за собой посторонний и крайне неприятный и шумный код. Например, рассмотрим раздел [ 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
повсюду в алгоритмах на основе диапазонов, является прямым следствием и [мотивацией] соглашения [начало, конец).
begin
и end
как int
s со значениями 0
и N
, соответственно, он идеально подходит. Возможно, это !=
условие более естественное, чем традиционное <
, но мы никогда не открывали его, пока не начали думать о более общих коллекциях.
++
-крименабельный шаблон итератора step_by<3>
, который будет иметь первоначально объявленную семантику.
!=
когда он должен использовать <
, то это ошибка. Кстати, этот король ошибки легко найти с помощью модульного тестирования или утверждений.
На самом деле, многие вещи, связанные с итераторами, внезапно приобретают гораздо больший смысл, если учесть, что итераторы указывают не на элементы последовательности, а между ними , с разыменованием доступа к следующему элементу прямо к нему. Тогда итератор «один конец» внезапно приобретает смысл:
+---+---+---+---+
| 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
находится справа от него.
foo[i]
) является сокращением для элемента сразу после позиции i
). Размышляя об этом, я задаюсь вопросом: может быть полезно для языка иметь отдельные операторы для «элемента сразу после позиции i» и «элемента непосредственно перед позицией i», поскольку многие алгоритмы работают с парами смежных элементов и говорят « Предметы по обе стороны от позиции i "могут быть чище, чем" Предметы в позициях i и i + 1 ".
begin[0]
(при условии, что итератор произвольного доступа) будет обращаться к элементу 1
, поскольку 0
в моем примере последовательности нет элемента .
start()
в вашем классе для запуска определенного процесса или чего-то еще, это будет раздражать, если оно конфликтует с уже существующим).
Почему Стандарт определяется end()
как конец за концом, а не как фактический конец?
Так как:
begin()
равно
end()
& end()
пока они не достигнуты.Потому что тогда
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++;
}
begin() == end()
.!=
вместо <
(меньше, чем) в условиях цикла, поэтому end()
удобно указывать на позицию в начале.