Разрешены ли итераторы end+1 для std::string?

Допустимо ли создавать итератор для end(str)+1 за std::string?
А если нет, то почему нет?

Этот вопрос ограничен C++11 и более поздними версиями, потому что, хотя до C++11 данные уже были сохранены в непрерывном блоке в любых, но редких реализациях игрушек POC, данные не должны были храниться таким образом.
И я думаю, что это может иметь все значение.

Существенная разница между std::string и любой другой стандартный контейнер, о котором я размышляю, состоит в том, что он всегда содержит на один элемент больше, чем его size, нулевой терминатор, для выполнения требований .c_str(),

21.4.7.1 методы доступа basic_string [string.accessors]

const charT* c_str() const noexcept;
const charT* data() const noexcept;

1 Возвращает: указатель p такой, что p + i == &operator[](i) для каждого i в [0,size()],
2 Сложность: постоянное время.
3 Требуется: программа не должна изменять никакие значения, хранящиеся в массиве символов.

Тем не менее, несмотря на то, что он должен гарантировать, что указанное выражение является допустимым, для согласованности и совместимости со строками с нулевым символом в конце, если ничего больше, единственный найденный мной пункт ставит под сомнение это:

21.4.1 общие требования basic_string [string.require]

4 Чароподобные объекты в basic_string Объект должен храниться смежно. То есть для любого basic_string объект s, личность &*(s.begin() + n) == &*s.begin() + n будет иметь место для всех значений n такой, что 0 <= n < s.size(),

(Все цитаты взяты из окончательного варианта C++14 (n3936).)

Связанный: Законный, чтобы перезаписать нулевой терминатор std::string?

4 ответа

TL;DR: s.end() + 1 является неопределенным поведением.


std::string странный зверь, в основном по историческим причинам

  1. Он пытается принести совместимость с C, где известно, что дополнительный \0 символ существует за пределами длины, сообщенной strlen,
  2. Он был разработан с интерфейсом на основе индекса.
  3. В последствии после слияния в стандартной библиотеке с остальной частью кода STL был добавлен интерфейс на основе итератора.

Это привело std::string В C++03 к номеру 103 добавились функции-члены, и с тех пор некоторые из них были добавлены.

Следовательно, следует ожидать расхождений между различными методами.


Уже в индексном интерфейсе появляются расхождения:

§21.4.5 [string.access]

const_reference operator[](size_type pos) const;
reference operator[](size_type pos);

1 / Требуется: pos <= size()

const_reference at(size_type pos) const;reference at(size_type pos);

5 / Броски: out_of_range если pos >= size()

Да, вы правильно прочитали, s[s.size()] возвращает ссылку на символ NUL, в то время как s.at(s.size()) бросает out_of_range исключение. Если кто-то говорит вам, чтобы заменить все виды использования operator[] от at потому что они безопаснее, остерегайтесь string ловушка...


Итак, что насчет итераторов?

§21.4.3 [string.iterators]

iterator end() noexcept;
const_iterator end() const noexcept;
const_iterator cend() const noexcept;

2 / Возвраты: Итератор, который является последним значением.

Чудесно мягкий.

Поэтому мы должны ссылаться на другие пункты. Указатель предлагается

§21.4 [basic.string]

3 / Итераторы, поддерживаемые basic_string являются итераторами с произвольным доступом (24.2.7).

в то время как §17.6 [требования], кажется, лишены всего, что связано. Таким образом, строковые итераторы являются просто старыми итераторами (вы, вероятно, можете почувствовать, куда это идет... но, поскольку мы зашли так далеко, давайте пройдем весь путь).

Это приводит нас к:

24.2.1 [iterator.requirements.general]

5 / Так же, как обычный указатель на массив гарантирует, что существует значение указателя, указывающее на последний элемент массива, так и для любого типа итератора есть значение итератора, которое указывает на последний элемент соответствующей последовательности. Эти значения называются прошлыми значениями. Значения итератора i для которого выражение *i определяется как разыменованный. Библиотека никогда не предполагает, что последние значения являются разыменованными. [...]

Так, *s.end() плохо сформирован.

24.2.3 [input.iterators]

2 / Таблица 107 - Требования к итератору ввода (в дополнение к итератору)

Список для предварительного условия ++r а также r++ тот r быть разыменованным

Ни прямые итераторы, ни двунаправленные итераторы, ни случайные итераторы не снимают это ограничение (и все они указывают, что они наследуют ограничения своего предшественника).

Также, для полноты, в 24.2.7 [random.access.iterators] Таблица 111 - Требования к итератору произвольного доступа (в дополнение к двунаправленному итератору) перечисляет следующую операционную семантику:

  • r += n эквивалентно [inc|dec] запоминанию rn раз
  • a + n а также n + a эквивалентны копированию a а затем применяя += n к копии

и аналогично для -= n а также - n,

таким образом s.end() + 1 является неопределенным поведением.

Возвращает: указатель p такой, что p + i == &operator[](i) для каждого i в [0,size()],

std::string::operator[](size_type i) указано возвращать "ссылку на объект типа charT со значением charT() когда i == size()Итак, мы знаем, что этот указатель указывает на объект.

5.7 утверждает, что "в целях [операторов + и -] указатель на объект без массива ведет себя так же, как указатель на первый элемент массива длиной один с типом объекта в качестве его типа элемента".

Таким образом, у нас есть объект, не являющийся массивом, и спецификация гарантирует, что один указатель после него будет представимым. Итак, мы знаем std::addressof(*end(str)) + 1 должен быть представимым.

Однако это не гарантия std::string::iteratorи нет никакой гарантии где-либо в спецификации, что делает его неопределенным поведением.

(Обратите внимание, что это не то же самое, что "плохо сформированный". *end(str) + 1 на самом деле хорошо сформирован.)

Итераторы могут реализовать логику проверки, которая выдает различные ошибки, когда вы делаете такие вещи, как увеличение end() итератор. Это фактически то, что делают итераторы отладки Visual Studios end(str) + 1,

#define _ITERATOR_DEBUG_LEVEL 2
#include <string>
#include <iterator>

int main() {
  std::string s = "ssssssss";
  auto x = std::end(s) + 1; // produces debug dialog, aborts program if skipped
}

А если нет, то почему нет?

для согласованности и совместимости со строками с нулевым символом в конце, если ничего больше

C++ определяет некоторые специфические вещи для совместимости с C, но такая обратная совместимость ограничивается поддержкой вещей, которые действительно могут быть написаны на C. C++ не обязательно пытается взять семантику C и заставить новые конструкции вести себя аналогичным образом. Должен std::vector распад на итератор, просто чтобы соответствовать поведению распада массива C?

я бы сказал end(std) + 1 остается неопределенным поведением, потому что нет смысла пытаться ограничить std::string итераторы таким образом. Нет унаследованного кода C, который делает это, с которым C++ должен быть совместим, и новый код должен быть предотвращен от этого.

Новый код не должен полагаться на него... почему? [...] Что не позволяет купить его в теории, и как это выглядит на практике?

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

На самом деле мне кажется, что поддержка end(str) + 1 имеет отрицательное значение, так как код, который пытается его использовать, будет по существу создавать ту же проблему, что и код C, который не может определить, когда учитывать нулевой терминатор или нет. C имеет достаточно отключения по одному размеру буфера ошибок для обоих языков.

std::basic_string<???> является контейнером над его элементами. Его элементы не включают конечный ноль, который добавляется неявно (он может включать в себя встроенные нули).

Это имеет большой смысл - "для каждого символа в этой строке", вероятно, не должен возвращать завершающий '\0', поскольку это действительно деталь реализации для совместимости с API стиля C.

Правила итераторов для контейнеров были основаны на контейнерах, которые не добавляли лишний элемент в конце. Изменение их для std::basic_string<???> без мотивации сомнительно; нужно нарушать рабочий режим, только если есть отдача.

Есть все основания полагать, что указатели на .data() а также .data() + .size() + 1 разрешены (я мог бы представить искаженную интерпретацию стандарта, которая сделала бы его недопустимым). Так что если вам действительно нужны итераторы только для чтения в содержимое std::stringВы можете использовать указатели на const-элементы (которые, в конце концов, являются своего рода итератором).

Если вы хотите редактируемые, то нет, нет способа получить действительный итератор до конца. Вы также не можете получить неconst ссылка на завершающий нуль на законных основаниях. На самом деле такой доступ явно плохая идея; если вы измените значение этого элемента, вы нарушите std::basic_stringинвариант нуль-окончание.

Чтобы существовал итератор до конца, константные и неконстантные итераторы для контейнера должны иметь другой допустимый диапазон или неконстантный итератор для последнего элемента, который может быть разыменован, но не написано должно существовать.

Я содрогаюсь от того, чтобы сделать такую ​​стандартную формулировку водонепроницаемой.

std::basic_string это уже беспорядок. Создание этого даже более странного приведет к стандартным ошибкам и будет иметь нетривиальную стоимость. Выгода действительно низкая; в тех немногих случаях, когда вам нужен доступ к указанному конечному нулю в диапазоне итераторов, вы можете использовать .data() и использовать полученные указатели в качестве итераторов.

Я не могу найти окончательного ответа, но косвенные данные указывают на то, что он не определен.

[string.insert]/15

constexpr iterator insert(const_iterator p, charT c);
Предварительные условия: pявляется допустимым итератором на *this.

Было бы неразумно ожидать, что это будет работать как итератор, и это действительно вызывает сбой как в libstdc++, так и в libc++.

Это означает end()+1не является допустимым итератором, что означает end()не является инкрементным.

Другие вопросы по тегам