Чому діапазони стандартного ітератора [початок, кінець] замість [почати, закінчити]?


204

Чому Стандарт визначає end()як минулий кінець, а не фактичний?


19
Я здогадуюсь, "бо так говорить стандарт" це не скоротить, правда? :)
Лучіан Григоре

39
@LuchianGrigore: Звичайно, ні. Це могло б підірвати нашу повагу до (людей, що стоять за) стандартом. Слід очікувати, що є причина для вибору, зробленого стандартом.
Керрек СБ

4
Коротше кажучи, комп’ютери не вважаються людьми. Але якщо вам цікаво, чому люди не належать до комп'ютерів, я рекомендую The Nothing that is: Natural History Zero для глибокого перегляду проблем, які люди мали, виявивши, що існує число, яке на менше ніж один.
Джон Макфарлейн

8
Оскільки існує лише один спосіб генерувати "останній", це часто не дешево, оскільки він має бути справжнім. Створення "ти впав з кінця скелі" завжди дешево, багато можливих уявлень зробляться. (пустота *) "ahhhhhhh" буде добре.
Ганс Пасант

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
Типовим С для ітерації циклу над масивом розміром N є "для (i = 0; i <N; i ++) a [i] = 0;". Тепер ви не можете висловити це безпосередньо за допомогою ітераторів - багато людей витрачали час, намагаючись зробити <змістом. Але майже однаково очевидно сказати "для (i = 0; i! = N; i ++) ..." Картування 0, щоб почати, і N до кінця, тому зручно.
Krazy Glew

3
@KrazyGlew: Я не ставив типів у свій приклад циклу навмисно. Якщо ви думаєте про beginі endяк ints зі значеннями 0і N, відповідно, це ідеально підходить. Можливо, це !=умова, більш природна, ніж традиційна <, але ми ніколи цього не виявляли, поки не почали думати про більш загальні колекції.
Керрек СБ

4
@KerrekSB: Я погоджуюся, що "ми ніколи не виявили, що [! = Краще], поки не почали думати про більш загальні колекції". ІМХО - це одне з того, що Степанов заслуговує на заслугу - виступаючи як хтось, хто намагався написати такі бібліотеки шаблонів перед STL. Однак я буду сперечатися про те, що "! =" Бути більш природним - або, скоріше, я стверджую, що! =, Мабуть, ввів помилок, які <би спіймали. Подумайте (i = 0; i! = 100; i + = 3) ...
Krazy Glew

@KrazyGlew: Ваш останній пункт дещо поза темою, оскільки послідовність {0, 3, 6, ..., 99} не є такою формою, про яку запитувала ОП. Якщо ви хотіли, щоб це було таким чином, вам слід написати ++нерозбірливий ітераторський шаблон step_by<3>, який тоді мав би оригінально рекламовану семантику.
Керрек СБ

@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в моєму прикладі послідовності немає жодного елемента .
кельтшк

Чому слово "почати" вживається, а не "починати"? Адже «почати» - це дієслово.
користувач1741137

@ user1741137 Я думаю, що "почати" мається на увазі абревіатура "початок" (що тепер має сенс). "початок" бути занадто довгим, "почати" звучить як приємна відповідність. "start" буде суперечити дієслову "start" (наприклад, коли вам потрібно визначити функцію start()у вашому класі для запуску певного процесу чи будь-якого іншого, це буде прикро, якщо воно суперечить уже існуючому).
Fareanor

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
Це сильно занижена відповідь. Приклади є стислими і прямими, і "Також" ніхто не сказав, і це речі, які здаються дуже очевидними в ретроспективі, але вражають мене, як одкровення.
підкреслити_19

@underscore_d: Дякую !! :)
користувач541686

btw, якщо я здаюся лицеміром за те, що не звертався з проханням, це тому, що я вже був у липні 2016 року!
підкреслюй_d

@underscore_d: ха-ха, я навіть не помітив, але дякую! :)
користувач541686

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