Проходят ли разыменованные итераторы за "одним зацикленным" итератором с неопределенным поведением массива?

Дано int foo[] = {0, 1, 2, 3}; Я хочу знать, являются ли недействительными итераторы, которые указывают за "один конец". Например: auto bar = cend(foo) + 1;

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

У меня все больше проблем с покупкой, например:

int* bar;

Неинициализирован, но, конечно, не вызывает неопределенного поведения, и при достаточном количестве попыток я уверен, что смогу найти экземпляр, в котором значение в этом неинициализированном bar имел то же значение, что и cend(foo) + 1,

Одно из самых больших заблуждений заключается в том, что я не спрашиваю о разыменовании cend(foo) + 1, Я знаю, что это было бы неопределенным поведением, и стандарт запрещает это. Но ответы, подобные этим: /questions/45588073/razreshenyi-li-iteratoryi-end1-dlya-stdstring/45588088#45588088 которых говорится только о том, что разыменование такого итератора является незаконным, не отвечают на вопрос.

Я также знаю, что C++ только гарантирует, что cend(foo) будет действительным, но это может быть numeric_limits<int*>::max(), в таком случае cend(foo) + 1 будет переполнен. Я не заинтересован в этом случае, если в стандарте это не вызвано как причина, по которой у нас не может быть итератора, прошедшего "один за другим". я знаю это int* на самом деле просто содержит целочисленное значение и как таковое может быть переполнено.

Я хотел бы привести цитату из достоверного источника о том, что перемещение итератора за пределы элемента "один конец" является неопределенным поведением.

4 ответа

Решение

Да, ваша программа имеет неопределенное поведение, если вы формируете такой указатель.

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

[C++14: 5.7/5]: Когда выражение, имеющее целочисленный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд-указатель указывает на элемент объекта массива, и массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующего и исходного элементов массива равна интегральному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P)+N что то же самое, N+(P)) а также (P)-N (где N имеет значение n), указывающее соответственно на i + n-й и i-n-й элементы объекта массива, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, выражение (P)+1 указывает один за последним элементом объекта массива, и, если выражение Q указывает один за последним элементом объекта массива, выражение (Q)-1 указывает на последний элемент объекта массива. Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива, оценка не должна вызывать переполнение;в противном случае поведение не определено.

Неинициализированный указатель - это не то же самое, потому что вы никогда не делали ничего, чтобы "получить" этот указатель, кроме объявления его (что, очевидно, допустимо). Но вы даже не можете оценить это (не разыскивать -оценить), не наполнив вашу программу неопределенным поведением. Нет, пока вы не присвоите ему правильное значение.

В качестве идентификатора я бы не назвал эти итераторы / указатели "за последние", термин в C++, который конкретно означает " один итератор / указатель за последние", который является допустимым (например,cend(foo) сам). Ты уже не в конце.;)

TL;DR - это неопределенное поведение для вычисления итератора после итератора "один за другим", потому что в процессе нарушается предварительное условие.


Легкость предоставила цитату, которая авторитетно охватывает указатели.

Для итераторов приращение за "конец" (one-past-the-last-element) вообще не запрещено, но запрещено для большинства различных типов итераторов:

Требования к итератору

Требования InputIterator

Требования к входному итератору и, в частности, единственное добавляемое условие, если оно является условием разыменования, включены посредством ссылки в прямой, двунаправленный и произвольный доступ.

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

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

Требования RandomAccessIterator

Как хорошо сказал @Random842:

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

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

Во многих современных системах указатели реализованы в плоском линейном пространстве. Даже в этих системах неопределенность формирования некоторых указателей может открыть ваш компилятор C++ для некоторых оптимизаций; например, int foo[5]; bool test(int* it1) { int* it2 = cend(foo); return it1 <= it2; } можно оптимизировать для true так как нет указателей, которые могут быть достоверно сопоставлены с it2 которые не меньше или равны ему.

В менее надуманных ситуациях (например, в некоторых циклах) это может сэкономить циклы в каждом цикле.

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

Сегментированная память является наиболее известной. В старых системах x86 каждый указатель представляет собой пару 16-битных значений. Местоположение, на которое они ссылаются в линейном 20-битном адресном пространстве, high << 4 + low, или же segment << 4 + offset,

Объекты живут внутри сегмента и имеют постоянное значение сегмента. Это означает, что все определенные указатель < Сравнения можно просто сравнить offset 16 младших бит. Они не должны делать эту математику (которая в то время была дорогой), они могут отбросить старшие 16 бит и сравнить значения смещения при заказе.

Существуют другие архитектуры, в которых код существует в параллельном адресном пространстве с данными (поэтому сравнение указателей кода с указателями данных может вернуть ложное равенство).

Правила довольно просты. Может создавать указатели на элементы в массивах и на один конец (это означает, что система сегментированной памяти не может создавать массивы, которые достигают самого конца сегмента).

Теперь ваша память не сегментирована, так что это не ваша проблема, верно? Компилятор может интерпретировать ваше формирование ptr+2 вдоль определенной ветви кода, чтобы означать, что ptr не является указателем на последний элемент массива и соответственно оптимизируется. Если это не так, ваш код может вести себя неожиданным образом.

И есть примеры реальных компиляторов, использующих подобные методы (если предположить, что код не использует неопределенное поведение, доказывать его инварианты, использовать выводы для изменения поведения до появления неопределенного поведения), если не этот конкретный случай. Неопределенное поведение может путешествовать во времени, даже если базовая аппаратная реализация "не будет иметь проблем с ним" без каких-либо оптимизаций.

Я не заинтересован в этом случае, если в стандарте это не вызвано как причина, по которой у нас не может быть итератора, прошедшего "один за другим". Я знаю, что int * на самом деле просто содержит целочисленное значение и поэтому может быть переполнен.

Стандарт не обсуждает причины неопределенности. У вас есть логика в обратном направлении: тот факт, что он не определен, является причиной того, что реализация может поместить объект в место, где выполнение такого действия могло бы вызвать переполнение. Если итератор "два за концом" должен был быть допустимым, то реализации должны были бы не помещать объект куда-либо, что могло бы вызвать переполнение такой операции.

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