Инкремент указателя на динамический массив размером 0 не определен?

34

AFAIK, хотя мы не можем создать массив статической памяти размером 0, но мы можем сделать это с динамическими:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

Как я читал, pдействует как элемент «один конец». Я могу напечатать адрес, на который pуказывает.

if(p)
    cout << p << endl;
  • Хотя я уверен, что мы не можем разыменовать этот указатель (last-last-element), как мы не можем использовать итераторы (past-last-element), но в чем я не уверен, является ли увеличение этого указателя p? Похоже ли неопределенное поведение (UB) с итераторами?

    p++; // UB?
Итачи Учива
источник
4
UB "... Любые другие ситуации (то есть попытки создать указатель, который не указывает на элемент того же массива или один за концом) вызывают неопределенное поведение ...." from: en.cppreference.com / w / cpp / language / operator_arithmetic
Ричард Криттен
3
Ну, это похоже на элемент std::vectorс 0. begin()уже равно, end()поэтому вы не можете увеличить итератор, который указывает на начало.
Phil1970
1
@PeterMortensen Я думаю, что ваше изменение изменило значение последнего предложения («В чем я уверен -> я не уверен, почему»), не могли бы вы перепроверить?
Фабио говорит восстановить Монику
@PeterMortensen: последний отредактированный абзац стал менее читабельным.
Итачи Учива

Ответы:

32

Указатели на элементы массивов могут указывать на действительный элемент или один за концом. Если вы увеличиваете указатель так, чтобы он проходил больше одного конца, поведение не определено.

Для вашего массива нулевого размера pон уже указывает на один конец, поэтому увеличение его не допускается.

См. C ++ 17 8.7 / 4 относительно +оператора ( ++имеет те же ограничения):

f выражение Pуказывает на элемент x[i]объекта массива xс n элементами, выражения P + Jи J + P(где Jимеет значение j) указывают на (возможно, гипотетический) элемент, x[i+j]если 0≤i + j≤n; в противном случае поведение не определено.

interjay
источник
2
Так что единственный случай x[i]такой же, как x[i + j]и когда оба iи jимеют значение 0?
Рами Йен
8
@RamiYen x[i]- это тот же элемент, как x[i+j]если бы j==0.
междюй
1
Тьфу, я ненавижу "сумеречную зону" семантики C ++ ... +1, хотя.
einpoklum
4
@ einpoklum-reinstateMonica: на самом деле нет сумеречной зоны. Это просто C ++, согласованный даже для случая N = 0. Для массива из N элементов существует N + 1 допустимых значений указателя, потому что вы можете указывать за массивом. Это означает, что вы можете начать с начала массива и увеличить указатель N раз, чтобы добраться до конца.
MSalters
1
@MaximEgorushkin Мой ответ о том, что язык в настоящее время позволяет. Обсуждение о том, что вы хотели бы разрешить, вместо этого не по теме.
междурядное
2

Я думаю, у вас уже есть ответ; Если вы посмотрите немного глубже: вы сказали, что инкрементным итератором является UB, таким образом: Этот ответ в том, что такое итератор?

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

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p указывает на первый элемент в arr

++ р; // p указывает на arr [1]

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

int * e = & arr [10]; // указатель сразу за последним элементом в arr

Здесь мы использовали оператор индекса для индексации несуществующего элемента; arr имеет десять элементов, поэтому последний элемент в arr находится в позиции индекса 9. Единственное, что мы можем сделать с этим элементом, это взять его адрес, который мы делаем для инициализации e. Как и внешний итератор (§ 3.4.1, стр. 106), внешний указатель не указывает на элемент. В результате мы не можем разыменовывать или увеличивать внешний указатель.

Это из C ++ primer 5 издание Lipmann.

Так что это UB не делай этого.

Raindrop7
источник
-4

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

Стандартная цитата, заданная interjay, является хорошей и указывает на UB, но, на мой взгляд, это всего лишь второй лучший результат, поскольку он имеет дело с арифметикой указатель-указатель (как ни странно, один явно UB, а другой нет). В этом вопросе есть параграф, касающийся операции:

[expr.post.incr] / [expr.pre.incr]
Операндом должен быть [...] или указатель на полностью определенный тип объекта.

О, подождите, полностью определенный тип объекта? Это все? Я имею в виду, действительно, типа ? Так тебе вообще не нужен объект?
Чтобы найти подсказку о том, что что-то там, возможно, не так четко определено, требуется немало чтения. Потому что до сих пор он читается так, как будто вам совершенно разрешено это делать, никаких ограничений.

[basic.compound] 3делает заявление о том, какой тип указателя может иметь, и, будучи не одним из трех других, результат вашей операции явно попадет под 3.4: неверный указатель .
Однако это не говорит о том, что вам не разрешено иметь неверный указатель. Напротив, в нем перечислены некоторые очень распространенные, нормальные условия (например, время окончания хранения), когда указатели регулярно становятся недействительными. Так что, по-видимому, это допустимо. И действительно:

[basic.stc] 4 Переадресация
через недопустимое значение указателя и передача недопустимого значения указателя в функцию освобождения имеют неопределенное поведение. Любое другое использование недопустимого значения указателя имеет поведение, определяемое реализацией.

Мы делаем «любое другое», так что это не неопределенное поведение, а определяемое реализацией, поэтому, как правило, допустимо (если реализация явно не говорит что-то другое).

К сожалению, это не конец истории. Хотя с этого момента чистый результат больше не меняется, он становится более запутанным, чем дольше вы ищете «указатель»:

[basic.compound]
Допустимое значение типа указателя объекта представляет собой адрес байта в памяти или нулевой указатель. Если объект типа T находится по адресу, то говорят, что [...] указывает на этот объект, независимо от того, как было получено значение .
[Примечание: Например, считается, что адрес, следующий за концом массива, указывает на несвязанный объект типа элемента массива, который может быть расположен по этому адресу. [...]].

Читайте как: ОК, кого это волнует! Пока указатель указывает где-то в памяти , я в порядке?

[basic.stc.dynamic.safety] Значение указателя является безопасно полученным указателем [бла-бла]

Читайте как: хорошо, безопасно выведено, что угодно. Это не объясняет, что это такое, и не говорит, что мне это действительно нужно. Безопасно-производные в-щеколду. Очевидно, у меня все еще могут быть указатели, не являющиеся безопасными, просто отлично. Я предполагаю, что разыменование их, вероятно, не будет хорошей идеей, но вполне допустимо иметь их. Это не говорит иначе.

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

О, так что это может не иметь значения, только то, что я думал. Но подождите ... "не может"? Значит, может и так . Откуда мне знать?

Альтернативно, реализация может иметь строгую безопасность указателя, и в этом случае значение указателя, которое не является безопасно полученным значением указателя, является недопустимым значением указателя, если только ссылка на полный объект не имеет длительности динамического хранения и ранее не была объявлена ​​достижимой

Подождите, так что даже возможно, что мне нужно вызывать declare_reachable()каждый указатель? Откуда мне знать?

Теперь вы можете преобразовать в intptr_t, что является четко определенным, давая целочисленное представление безопасно полученного указателя. Для которого, конечно, являясь целым числом, вполне законно и четко определено увеличивать его по своему усмотрению.
И да, вы можете преобразовать intptr_tобратно в указатель, который также четко определен. Просто, не будучи исходным значением, больше не гарантируется, что у вас есть безопасный производный указатель (очевидно). Тем не менее, в целом, к букве стандарта, будучи определяемой реализацией, это на 100% законно:

[expr.reinterpret.cast] 5
Значение целочисленного типа или типа перечисления может быть явно преобразовано в указатель. Указатель преобразуется в целое число достаточного размера [...] и обратно в исходное значение того же типа указателя [...]; в противном случае отображения между указателями и целыми числами определяются реализацией.

Подвох

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

Так что это основа «реализация определена». Это и тот факт, что увеличение указателя в любое время, как вам угодно, может, конечно, вызвать переполнение, с которым стандарт не хочет иметь дело. Конечное адресное пространство приложения может не совпадать с местом переполнения, и вы даже не знаете, существует ли такая вещь, как переполнение для указателей в конкретной архитектуре. В общем, это кошмарный беспорядок, не имеющий никакого отношения к возможным выгодам.

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

Damon
источник
1
Ваша логика несовершенна. "Так тебе вообще не нужен объект?" неверно истолковывает Стандарт, сосредотачиваясь на одном правиле. Это правило касается времени компиляции, является ли ваша программа правильно сформированной. Есть другое правило о времени выполнения. Только во время выполнения вы можете говорить о существовании объектов по определенному адресу. ваша программа должна соответствовать всем правилам; правила времени компиляции во время компиляции и правила времени исполнения во время выполнения.
MSalters
5
У вас есть аналогичные недостатки логики с «Хорошо, кого это волнует! Пока указатель указывает где-то в памяти, я в порядке?». Нет. Вы должны следовать всем правилам. Сложный язык о том, что «конец одного массива является началом другого массива» просто дает реализации возможность распределять память непрерывно; ему не нужно сохранять свободное пространство между выделениями. Это означает, что ваш код может иметь одинаковое значение A как конец одного объекта массива, так и начало другого.
MSalters
1
«Ловушка» - это не то, что можно описать поведением, определяемым реализацией. Обратите внимание, что interjay нашел ограничение на +оператор (из которого происходит ++поток), что означает, что указание после «один за концом» не определено.
Мартин Боннер поддерживает Монику
1
@PeterCordes: Пожалуйста, прочитайте basic.stc, параграф 4 . В нем говорится «Неопределенное поведение [...] косвенного направления. Любое другое использование недопустимого значения указателя имеет поведение, определяемое реализацией » . Я не путаю людей, используя этот термин для другого значения. Это точная формулировка. Это не неопределенное поведение.
Деймон
2
Вряд ли вы нашли лазейку для постинкремента, но вы не цитируете полный раздел о том, что делает постинкремент. Я не собираюсь сейчас в этом разбираться. Договорились, что если таковой есть, он непреднамеренный. В любом случае, как бы хорошо это ни было, если бы ISO C ++ определял больше вещей для моделей с плоской памятью, @MaximEgorushkin, есть и другие причины (например, обтекание указателя) для запрета произвольных вещей. См. Комментарии к статье. Должны ли сравнения указателей быть подписанными или неподписанными в 64-разрядной версии x86?
Питер Кордес