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

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

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

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

21.4.7.1 basic_string accessors [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 Объекты char в объекте basic_string должны храниться смежно. То есть для любого basic_string объекта s идентификатор &*(s.begin() + n) == &*s.begin() + n будет выполняться для всех значений n таких, что 0 <= n < s.size().

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

Связано: Право на перезаписывание нулевого терминатора std::string?

Ответы

Ответ 1

TL; DR: s.end() + 1 - поведение undefined.


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

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

Это привело std::string в С++ 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 - Требования ввода итератора (в дополнение к Iterator)

Перечислите предварительное условие ++r и r++, чтобы r было разуплотняемым.

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

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

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

и аналогично для -= n и - n.

Таким образом, s.end() + 1 - это поведение undefined.

Ответ 2

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

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

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

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

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

(Обратите внимание, что это не то же самое, что "плохо сформированный". *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 и делать некоторые новые конструкции по-разному. Должен ли std::vector распадаться на итератор только для того, чтобы быть совместимым с поведением распада массива C?

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

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