Аннулирование итератора с помощью `std :: string :: begin ()` / `std :: string :: end ()`?

#include <string>
#include <iostream>

int main() {
    std::string s = "abcdef";

    std::string s2 = s;

    auto begin = const_cast<std::string const &>(s2).begin();
    auto end = s2.end();

    std::cout << end - begin << '\n';
}

Этот код смешивает результат begin() const с результатом end(). Ни одна из этих функций не может делать недействительными какие-либо итераторы. Однако мне любопытно, действительно ли требование end() не аннулировать переменную итератора begin означает, что переменная begin может использоваться с end.

Рассмотрим C ++ 98, реализацию std::string с функцией копирования при записи; неконстантные функции begin() и end() вызывают копирование внутреннего буфера, потому что результат этих функций может использоваться для изменения строки. Таким образом, begin, приведенный выше, начинает действовать как для s, так и для s2, но использование неконстантного члена end() приводит к тому, что он больше не действует для s2, контейнера, который его создал.

Приведенный выше код дает «неожиданные» результаты с реализацией копирования при записи, такой как libstdc ++. Вместо end - begin, совпадающего с s2.size(), libstdc ++ производит другое число.

  • Означает ли то, что begin больше не является действительным итератором в s2, контейнере, из которого он был получен, «аннулирует» итератор? Если вы посмотрите на требования к итераторам, все они, похоже, соблюдаются для этого итератора после вызова .end(), поэтому, возможно, begin по-прежнему квалифицируется как действительный итератор и, следовательно, не был признан недействительным?

  • Хорошо ли определен приведенный выше код в C ++ 98? Что в C ++ 11 запрещает реализации копирования при записи?

Судя по моему собственному краткому чтению спецификаций, он кажется недостаточно определенным, так что не может быть никакой гарантии, что результаты begin() и end() могут когда-либо использоваться вместе, даже без смешивания версий с константой и неконстантой.


person bames53    schedule 26.02.2015    source источник
comment
Причина, по которой C ++ 11 явно запретил COW, заключается именно в этой проблеме: ваш код соответствует требованиям и должен приводить к 6, но, очевидно, нет. Реализация COW несовместима.   -  person Lightness Races in Orbit    schedule 26.02.2015
comment
libc ++ понимает это правильно. Live.   -  person Baum mit Augen    schedule 26.02.2015
comment
@BaummitAugen За некоторое определение права. Код в вопросе не является законным до C ++ 11, и он не будет работать (или не гарантирован) с библиотеками до C ++ 11 (включая стандартную библиотеку, поставляемую с g ++). Библиотека не ошибается, если выходит из строя; код есть.   -  person James Kanze    schedule 26.02.2015
comment
@JamesKanze Правильно, как определено стандартом, который я скомпилировал, конечно. Мой комментарий был не ответом, а комментарием.   -  person Baum mit Augen    schedule 27.02.2015


Ответы (4)


Как вы говорите, C ++ 11 отличается от более ранних версий в этом отношении. В C ++ 11 нет проблем, потому что все попытки разрешить копирование при записи были удалены. В версиях до C ++ 11 ваш код приводит к неопределенному поведению; вызов s2.end() может аннулировать существующие итераторы (и сделал, а может быть, и сейчас делает в g ​​++).

Обратите внимание, что даже если s2 не был копией, стандарт позволил бы аннулировать итераторы. Фактически, компакт-диск для C ++ 98 даже сделал такие вещи, как f( s.begin(), s.end() ) или s[i] == s[j] неопределенное поведение. Это было реализовано только в последнюю минуту и ​​исправлено так, что только первый вызов begin(), end() или [] мог сделать итераторы недействительными.

person James Kanze    schedule 26.02.2015
comment
Ссылки, указатели и итераторы, относящиеся к элементам последовательности basic_string, могут быть признаны недействительными при следующих применениях этого объекта basic_string: Вызов неконстантных функций-членов, кроме operator [] (), at (), begin (), rbegin ( ), end () и rend (). Из C ++ 03. - person Lightness Races in Orbit; 26.02.2015

Код в порядке: реализация CoW в значительной степени требуется для отмены совместного использования, когда существует опасность для итератора или сохраняется ссылка на элемент. То есть, когда у вас есть что-то, что обращалось к элементу в одной строке, и его копия пытается сделать то же самое, то есть использовать итератор или оператор нижнего индекса, это должно быть отменено. Он может знать о своих итераторах и обновлять их по мере необходимости.

Конечно, в параллельной системе практически невозможно сделать все это без гонок данных, но до C ++ 11 гонок данных не было.

person Dietmar Kühl    schedule 26.02.2015
comment
Код неверен, поскольку отмена совместного использования в вызове s2.end() сделает недействительным итератор, возвращенный предыдущим вызовом s1.begin() (через константную ссылку). (Кроме того, конечно: вы забыли пару слов в последнем предложении. Используйте мьютекс правильно, и это просто, чтобы избежать гонок данных. То, что вы, несомненно, имеете в виду, почти невозможно сделать без гонок данных и < / i> с приемлемой производительностью.) - person James Kanze; 26.02.2015
comment
@JamesKanze: если отмена совместного использования при использовании s2.end() сделает недействительными вещи, отмена совместного использования должна произойти при вызове s2.begin(). - person Dietmar Kühl; 26.02.2015
comment
Не так, как я понимаю. Его вызов begin() осуществляется через const lvalue, поэтому он звонит begin() const. Отмена совместного использования сделает итераторы недействительными, а реализации не разрешено аннулировать итераторы при вызове begin() const. - person James Kanze; 26.02.2015
comment
@JamesKanze: правила, по которым итераторы могут быть признаны недействительными, на самом деле не изменились. При внедрении CoW всегда нужно было отслеживать, был ли взят итератор или ссылка на элемент. После взятия первого итератора или ссылки на элемент во второй строке его необходимо отменить: на данный момент нет другого итератора или ссылки, которые могут быть признаны недействительными. - person Dietmar Kühl; 26.02.2015
comment
После взятия первого неконстантного итератора или ссылки на элемент реализация должна отменить общий доступ. Это то, о чем идет речь в последнем пункте §21.3. / 5. - person James Kanze; 28.02.2015

Что касается N3337 (который по сути идентичен C ++ 11), в спецификации указано ([string.require] / 4 ):

Ссылки, указатели и итераторы, относящиеся к элементам последовательности basic_string, могут быть признаны недействительными при следующих применениях этого объекта basic_string:
[...]
- Вызов неконстантных функций-членов, кроме operator [], at, front, back, begin, rbegin, end и rend.

По крайней мере, как я читал, это означает, что вызов begin или end не может аннулировать какие-либо итераторы. Хотя это не указано напрямую, я бы также принял это как означающее, что никакой вызов функции-члена const не может сделать недействительными любые итераторы.

Эта формулировка остается неизменной, по крайней мере, до n4296.

person Jerry Coffin    schedule 26.02.2015
comment
n4296 постдатирует C ++ 14, поэтому это не отвечает на вопрос о C ++ 98 и C ++ 11. Однако в этих стандартах делается такой же вывод из-за той же (или аналогичной) формулировки. - person Lightness Races in Orbit; 26.02.2015
comment
Тот же текст существует в C ++ 98 и C ++ 11, однако, если вы посмотрите на требования к итераторам, все они, похоже, сохраняют мою переменную begin после вызова end(). Таким образом, похоже, что begin технически вообще не был признан недействительным, хотя это не соответствует результату end(). Просто, похоже, нет никаких требований к begin() или end(), требующих, чтобы их результаты можно было использовать вместе. - person bames53; 26.02.2015
comment
Требования в C ++ до C ++ 11 существенно отличаются. Через CD2 C ++ 98 любой вызов неконстантных [], at(), begin() или end() может сделать недействительными итераторы, ссылки и указатели. Между CD2 и C ++ 98 комитет попытался исправить это, заявив, что только первый вызов может сделать итераторы недействительными. В C ++ 11 они изменили его, чтобы сказать, что вызов невозможен, что фактически запретило копирование при записи. В представленном коде программа получает итератор через вызов begin() const, затем вызывает end(), что до C ++ 11 могло сделать недействительным первый итератор. - person James Kanze; 26.02.2015

С ++ 98 [lib.basic.string] / 5 утверждает:

Ссылки, указатели и итераторы, относящиеся к элементам последовательности basic_string, могут быть признаны недействительными при следующих применениях объекта basic_string:

  • В качестве аргумента для функций, не являющихся членами swap(), operator>>() и getline().

  • Как аргумент basic_string::swap().

  • Вызов функций-членов data() и c_str().

  • Вызов неконстантных функций-членов, кроме operator[](), at(), begin(), rbegin(), end() и rend().

  • После любого из вышеперечисленных применений, кроме форм insert() и erase(), которые возвращают итераторы, первый вызов неконстантных функций-членов operator[](), at(), begin(), rbegin(), end() или rend().

Поскольку конструктор s2 является неконстантной функцией-членом, он соответствует вызову неконстантного s2.end() - первого такого вызова в соответствии с последним маркером выше - для аннулирования итераторов. Таким образом, программа не имеет определенного поведения в соответствии с C ++ 98.

Я не буду комментировать С ++ 11, поскольку я думаю, что другие ответы ясно объясняют, что программа определила поведение в этом контексте.

person Casey    schedule 26.02.2015