Разрешены ли итераторы 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
странный зверь, в основном по историческим причинам
- Он пытается принести совместимость с C, где известно, что дополнительный
\0
символ существует за пределами длины, сообщеннойstrlen
, - Он был разработан с интерфейсом на основе индекса.
- В последствии после слияния в стандартной библиотеке с остальной частью кода 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] запоминаниюr
n
раз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()
не является инкрементным.