Чому Стандарт визначає 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 (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()
зручно вказувати на позицію, розташовану в кінці.