Является ли стирание конечного итератора упущением в стандарте или дизайнерским решением?

Контейнеры стандартной библиотеки позволяют нам erase использовать диапазоны, обозначенные итераторами first и last.

std::vector<foo> bar;
//         first it     last it
bar.erase(bar.begin(), bar.end());

Стандарт говорит, что итератор first должен быть действительным и разыменовываемым, тогда как last должен быть только действительным. Однако, если first == last, то first не нужно разыменовывать, потому что erase не является операцией. Это означает, что следующее является законным:

bar.erase(bar.end(), bar.end());

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

bar.erase(bar.end());

Почему это не просто бездействие? Является ли это недосмотром комитета по стандартам, который будет устранен в будущем пересмотре языка, или это преднамеренное дизайнерское решение, в котором я не вижу смысла?

Насколько я вижу, это не дает никаких преимуществ, но создает дополнительные головные боли при выполнении чего-то вроде следующего:

bar.erase(std::find(bar.begin(), bar.end(), thing));

person Fibbs    schedule 17.04.2016    source источник
comment
Он должен был бы сравнить итератор, так что каждый, кто никогда не проходит через итератор конца, теперь будет выполнять дополнительную бесполезную проверку. Диапазон, который в любом случае нужно сравнить, чтобы знать, когда прекратить стирание.   -  person chris    schedule 17.04.2016
comment
@chris: Верно, и это должен быть ответ.   -  person Lightness Races in Orbit    schedule 17.04.2016
comment
@LightnessRacesinOrbit, на самом деле это было скорее предположение. Это одна маленькая проверка.   -  person chris    schedule 17.04.2016
comment
@chris: Ну, в любом случае, это ответ, а не комментарий, поэтому, пожалуйста, разместите его там, где его можно будет проверить и, возможно, принять! Спасибо   -  person Lightness Races in Orbit    schedule 17.04.2016
comment
Я думаю, это имеет смысл, но это не помогает поддерживать согласованность. Я бы принял это как ответ, если бы вы могли процитировать что-то/кого-то официального, утверждающего, что дополнительная проверка является обоснованием поведения.   -  person Fibbs    schedule 17.04.2016


Ответы (2)


C++ имеет привычку оставлять любую дополнительную работу в своей стандартной библиотеке, также известную как «Вы не платите за то, что не используете». В этом случае версия с одним итератором должна была бы сравнить итератор с конечным итератором. В большинстве случаев, когда эта проверка избыточна, это дополнительная работа, которая не используется, хотя и небольшая часть работы.

erase(iterator it):
    if it != end: // we can't avoid this check now
        // erase element

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

erase(iterator first, iterator last):
    while first != last:
        erase(first++);

Ему нужен какой-то способ узнать, когда прекратить стирание. В некоторых случаях это может быть умнее, например, memmoveing блок памяти для перезаписи стертой памяти без ветвления, но это происходит только в определенных сценариях.


Также обратите внимание, что гораздо проще собрать проверенную версию из этого, чем наоборот:

checked_erase(Container cont, iterator it):
    if it != cont.end():
        cont.erase(it);
person chris    schedule 17.04.2016
comment
Я принял ответ, потому что это наиболее вероятная причина такого поведения, и других ответов, похоже, не предвидится. Однако я изо всех сил пытаюсь представить себе ситуацию, когда я буду обрабатывать один итератор и точно знаю, что это не конец() без проверки. - person Fibbs; 17.04.2016
comment
@Fibbles, вы удаляете максимальный элемент из контейнера, который, как вы знаете, не пуст. Или первый элемент. Возможно, у вас есть библиотека, которая возвращает optional<iterator> вместо конца, и вы выполняете (упрощенная операция) opt.do_if_present(it -> cont.erase(it)); При разработке библиотеки обычно трудно предугадать, насколько широки варианты использования. Часто бывает приятно учитывать способы использования, о которых вы не можете думать, но кто-то неизбежно их придумает. - person chris; 17.04.2016

Почему это не просто бездействие?

С чего бы это быть неоперативным? Вы вызываете метод, специально предназначенный для удаления одного элемента, и вызываете его таким образом, что он не может удалить ни один элемент. Это не имеет никакого смысла. Вы можете возразить, что он должен генерировать исключение вместо неопределенного поведения, но я не вижу в вашем вопросе веских аргументов в пользу того, чтобы сделать его четко определенным как ничего не делающее, поэтому я не вижу причин думать, что это может быть оплошностью.

Ваш пример неубедителен для меня: пока элемент уникален в векторе, его уже легко выразить в простой форме, используя std::remove, а не std::find. Если элемент не уникален, укажите, сколько элементов вы хотите удалить.

Посмотрите на это с другой стороны: erase(it) эквивалентно erase(it, it+1) для всех итераторов. Включая конечный итератор. Создание специального исключения для четкого определения erase(it) для конечного итератора, где erase(it, it+1) было бы неопределенным, привело бы к несогласованности.

person Community    schedule 17.04.2016
comment
Рассмотрим вектор, в котором элемент может быть либо уникальным, либо не существовать. Использование std::remove будет без необходимости искать весь вектор даже после того, как он найдет уникальный элемент, тогда как std::find остановится, как только найдет элемент. Конечно, правильный метод, вероятно, будет чем-то вроде std::unique в сочетании со стиранием. Пример std::find был первым, о чем я подумал. - person Fibbs; 17.04.2016
comment
@Fibbles Вы также можете использовать remove_if с предикатом, который прекращает сравнение после обнаружения совпадения. - person ; 17.04.2016