Инкремент указателя на динамический массив размером 0 не определен? - PullRequest
30 голосов
/ 04 ноября 2019

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

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

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

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

    p++; // UB?
    

Ответы [ 3 ]

30 голосов
/ 04 ноября 2019

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

Для вашего массива 0 размера 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;в противном случае поведение не определено.

1 голос
/ 05 ноября 2019

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

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

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

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

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

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

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

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

Это из публикации C ++ primer 5 от Lipmann.

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

0 голосов
/ 04 ноября 2019

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Улов

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

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

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

...