Арифметика указателей с двумя разными буферами

Рассмотрим следующий код:

int* p1 = new int[100];
int* p2 = new int[100];
const ptrdiff_t ptrDiff = p1 - p2;

int* p1_42 = &(p1[42]);
int* p2_42 = p1_42 + ptrDiff;

Теперь, гарантирует ли Стандарт, что p2_42 указывает на p2[42]? Если нет, то всегда ли это верно для кучи Windows, Linux или веб-сборки?

Ответы

Ответ 1

Чтобы добавить стандартную цитату:

expr.add # 5

Когда два выражения указателя P и Q вычитаются, тип результата является определяемым реализацией знаковым целочисленным типом; этот тип должен быть того же типа, который определен как std::ptrdiff_t в заголовке <cstddef> ([support.types]).

  • (5.1) Если P и Q оба имеют нулевые значения указателя, результат равен 0.

  • (5.2) В противном случае, если P и Q указывают соответственно на элементы x[i] и x[j] одного и того же объекта массива x, выражение P - Q имеет значение i−j.

  • (5.3) В противном случае поведение не определено. [Примечание: если значение i−j не находится в диапазоне представимых значений типа std::ptrdiff_t, поведение не определено. - конец примечания]

(5.1) не применяется, так как указатели не являются nullptrs. (5.2) не применяется, потому что указатели не находятся в одном массиве. Итак, мы остались с (5.3) - UB.

Ответ 2

const ptrdiff_t ptrDiff = p1 - p2;

Это неопределенное поведение. Вычитание между двумя указателями хорошо определено, только если они указывают на элементы в одном и том же массиве. ([expr.add] ¶5.3).

Когда два выражения указателя P и Q вычитаются, тип результата является определяемым реализацией знаковым целочисленным типом; этот тип должен быть того же типа, который определен как std::ptrdiff_t в заголовке <cstddef> ([support.types]).

  • Если P и Q оба имеют нулевые значения указателя, результат равен 0.
  • В противном случае, если P и Q указывают соответственно на элементы x[i] и x[j] одного и того же объекта массива x, выражение P - Q имеет значение i−j.
  • В противном случае поведение не определено

И даже если был какой-то гипотетический способ получить это значение законным способом, даже это суммирование недопустимо, так как даже суммирование указатель + целое число ограничено, чтобы оставаться внутри границ массива ([expr.add] ¶4.2)

Когда выражение J, имеющее целочисленный тип, добавляется или вычитается из выражения P типа указателя, результат имеет тип P

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

Ответ 3

Третья строка - Неопределенное Поведение, поэтому Стандарт разрешает все что угодно после этого.

Разрешается только вычесть два указателя, указывающие на (или после) один и тот же массив.

Windows или Linux на самом деле не актуальны; компиляторы и особенно их оптимизаторы - вот что ломает вашу программу. Например, оптимизатор может распознать, что p1 и p2 оба указывают на начало int[100] поэтому p1-p2 должно быть 0.

Ответ 4

Стандарт допускает реализации на платформах, где память разделена на отдельные области, которые не могут быть достигнуты друг от друга с помощью арифметики указателей. В качестве простого примера, некоторые платформы используют 24-битные адреса, которые состоят из 8-битного номера банка и 16-битного адреса в банке. Добавление одного к адресу, который идентифицирует последний байт банка, даст указатель на первый байт этого же банка, а не на первый байт следующего банка. Этот подход позволяет вычислять адресную арифметику и смещения с использованием 16-битной математики, а не 24-битной математики, но требует, чтобы ни один объект не выходил за границы банка. Такой дизайн наложил бы некоторую дополнительную сложность на malloc и, вероятно, привел бы к большей фрагментации памяти, чем это происходило бы в противном случае, но пользовательскому коду обычно не нужно было бы заботиться о разделении памяти на банки.

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

Ответ 5

Если нет, то всегда ли это верно для кучи Windows, Linux или веб-сборки?

Даже если бы это было разрешено (это не потому, что они разные объекты), то

const ptrdiff_t ptrDiff = p1 - p2;

должно быть

const ptrdiff_t ptrDiff = p2 - p1;

так что это никогда не правда.