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

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

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 или кучи webassembly?

Нет даже гарантии, что объекты int выровнены по sizeof(int) (это касается всех известных мне ABI, но почти во всех правилах программирования есть исключения, поэтому некоторые ABI могут быть не такими); когда это не так, код, очевидно, не может гарантировать работу.

curiousguy 29.01.2019 08:57

@curiousguy Нет особых причин не выравнивать границы байтов в Intel, кроме производительности. Если бы вместо int мы использовали struct i5 { int i[5]; };, на практике p1 и p2 не были бы выровнены по sizeof(i5).

Martin Bonner supports Monica 29.01.2019 11:45

Дополнительный вопрос (хотя заданный ранее): Каково обоснование ограничений на арифметику или сравнение указателей?

xskxzr 30.01.2019 04:25
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
51
3
2 622
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

const ptrdiff_t ptrDiff = p1 - p2;

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

When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined as std::ptrdiff_­t in the <cstddef> header ([support.types]).

  • If P and Q both evaluate to null pointer values, the result is 0.
  • Otherwise, if P and Q point to, respectively, elements x[i] and x[j] of the same array object x, the expression P - Q has the value i−j.
  • Otherwise, the behavior is undefined

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

When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.

  • If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
  • Otherwise, if P points to element x[i] of an array object x with n elements,81 the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) element x[i+j] if 0≤i+j≤n and the expression P - J points to the (possibly-hypothetical) element x[i−j] if 0≤i−j≤n.
  • Otherwise, the behavior is undefined.

Есть ли причина, по которой стандарт позволяет вам создавать указатель на элемент после конца массива?

Vaelus 28.01.2019 18:02

@Vaelus Это упрощает написание циклов, которые увеличивают указатель на каждом шаге. Например, в противном случае for (char *x = xs; x < (xs + sizeof(xs)); x++) {...} был бы недопустимым, поскольку он увеличивает значение x после конца своего массива непосредственно перед прерыванием.

amalloy 28.01.2019 18:24

@amalloy было бы недопустимым, потому что он увеличивает x после конца своего массива непосредственно перед прерыванием Это станет незаконным до первого приращения — в xs + sizeof(xs).

Language Lawyer 28.01.2019 18:26

@LanguageLawyer Но это явно разрешено, или я неправильно понимаю? Вы можете указать на гипотетический элемент массива, следующий за концом (при условии, что вы не разыменовываете его), поэтому разрешены как xs + sizeof(xs), так и x, равные этому значению.

Max Langhof 29.01.2019 09:36

@MaxLanghof: AFAICT LanguageLawyer просто говорит, что _если бы xs + sizeof(xs) был незаконным (НО ЭТО НЕ ТАК), вы бы получили UB даже при первой оценке условия, непосредственно перед увеличением, поскольку именно там подвыражение xs + sizeof(xs) оценивается для первый раз. При этом, как показано выше, создание указателя на элемент «один-последний» явно разрешено (если вы не разыменовываете его) и является общепринятой идиомой.

Matteo Italia 29.01.2019 09:47

@MaxLanghof Как было правильно прокомментировано, мы обсуждаем гипотетическую ситуацию, если не разрешено указывать сразу после последнего элемента. @amalloy сказал, что в этом случае приращение после последней итерации станет недействительным, и я исправил, что xs + sizeof(xs) вызовет UB еще до первого приращения.

Language Lawyer 29.01.2019 10:20

@LanguageLawyer О да, я как-то полностью пропустил комментарий Вэлуса - это должно было быть что-то очевидное. Извините за неприятности.

Max Langhof 29.01.2019 10:22

@Vaelus: у каждого объекта есть начальный адрес и конечный адрес, причем последний указывает «сразу за» объектом. Оба адреса допустимы, но только начальный адрес можно использовать непосредственно для доступа к объекту (конечный адрес можно использовать для вычисления начального адреса, который затем можно использовать для адресации объекта).

supercat 30.01.2019 08:23

Третья строка — Undefined Behavior, поэтому стандарт разрешает все, что угодно после нее.

Допустимо только вычитание двух указателей, указывающих на (или после) один и тот же массив.

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

Поскольку третья строка — Undefined Behavior, стандарт также разрешает все до :(

Mooing Duck 29.01.2019 00:27
Ответ принят как подходящий

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

expr.add#5

When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined as std::ptrdiff_­t in the <cstddef> header ([support.types]).

  • (5.1) If P and Q both evaluate to null pointer values, the result is 0.

  • (5.2) Otherwise, if P and Q point to, respectively, elements x[i] and x[j] of the same array object x, the expression P - Q has the value i−j.

  • (5.3) Otherwise, the behavior is undefined. [ Note: If the value i−j is not in the range of representable values of type std::ptrdiff_­t, the behavior is undefined. — end note  ]

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

5.2 может применяться, если у вас есть специальный распределитель (я думаю)

sudo rm -rf slash 28.01.2019 13:25

@sudorm-rfslash: Опасная территория. Массивы — это объекты, но распределители создают только хранилище, а не объекты. Два массива — это два разных объекта. В промежутке реализация может зарезервировать пространство для собственных служебных данных независимо от используемого распределителя. Обычно реализация хранит количество элементов, которые необходимо уничтожить. (В Стандартах ведутся споры о том, как формально массивы могут увеличиваться поэлементно, но в основном это касается std::vector. new[100] — это одноразовая операция)

MSalters 28.01.2019 13:59

@sudorm-rfslash 5.2 не применяется даже для 2 разных подмассивов (подобъектов одного полного объекта) многомерного массива (например, int a[2][3]; &a[1][0] - &a[0][2]; — это UB), и вы хотите, чтобы он применялся в случае, когда 2 полных объекта массива создаются в одном буфере ( например массив unsigned char)...

Language Lawyer 28.01.2019 14:44

@MSalters Почему люди в любом случае так настаивают на выполнении арифметических операций с указателями напрямую, просто приведите их к uintptr_t, а затем добавьте и вычтите сколько душе угодно.

Joker_vD 29.01.2019 08:01

@Joker_vD: Это не обязательно имеет смысл. uintptr_t имеет достаточно битов для хранения значения указателя, вот и все.

MSalters 29.01.2019 09:22

Отображение @Joker_vD от целых чисел к указателям и наоборот определяется реализацией, и единственное, что гарантируется, это то, что приведение от указателя к целому числу и обратно должно давать одно и то же значение указателя (если есть целочисленный тип подходящего размера). А GCC, например, не гарантирует больше абсолютного минимума, требуемого стандартом, и говорит, что использование целых чисел для обхода арифметических ограничений указателя — это UB gcc.gnu.org/onlinedocs/gcc/….

Language Lawyer 29.01.2019 10:25

@LanguageLawyer GCC делает вид, что целые значения из приведения указателей имеют «происхождение», которое, насколько мне известно, является выдуманным утверждением, не основанным ни на чем в стандартном стандарте.

curiousguy 29.01.2019 12:05

@curiousguy это выдуманное утверждение не противоречит стандарту, поскольку в любом случае это территория UB.

Ruslan 29.01.2019 12:55

@MSalters Но арифметика указателей в большинстве случаев гарантированно имеет значение меньше. Черт, чтение значения указателя из правильно инициализированной переменной типа указателя может быть UB, чего не происходит с целыми числами.

Joker_vD 29.01.2019 12:55

@Ruslan Что именно имеет УБ? GCC полностью разработал идею о том, что целое число имеет происхождение, подобное указателю. И кстати, происхождение - это концепция, выдуманная комитетом C, не основанная на каком-либо фактическом пункте стандарта, как написано. И происхождение указателя противоречит тому, что указатели фактов являются тривиальными типами. Вы не можете иметь происхождение и по-прежнему утверждать, что значение является функцией представления. Дело в мошенничестве.

curiousguy 29.01.2019 15:09

Происхождение значения указателя @curiousguy имеет значение только тогда, когда вы выполняете целочисленную арифметику, чтобы нарушить правило о UB в арифметике указателя. Если вы не попытаетесь обратно преобразовать целое число в указатель, UB не получится — вы просто получите целочисленные результаты, которые были бы, если бы вы написали это на ассемблере.

Ruslan 29.01.2019 15:18

@curiousguy Комитет объяснил свои намерения и, насколько мне известно, >>> формулировку намерений.

Language Lawyer 29.01.2019 21:46

@LanguageLawyer Я согласен с тем, что хорошо понятные намерения важнее, чем точные слова, особенно. когда есть согласие по поводу того, что нужно делать. Я согласен с тем, что некоторые сумасшедшие практики не должны поддерживаться намеренно, например, while(int i=rand(); i!=&x; i=rand()); int *p = (int*)i; // same as p=&x, и по этому поводу существует консенсус. Трудно сказать, что должно поддерживаться в языке программирования высокого/низкого уровня; низкоуровневое программирование конфликтует с высокоуровневой оптимизацией. Я не думаю, что C или C++ хороши в сочетании обоих.

curiousguy 29.01.2019 23:09

@Ruslan: Стандарт не пытается предписывать, чтобы компиляторы подходили для какой-либо конкретной цели, но вместо этого ожидает, что качественные компиляторы, утверждающие, что они подходят для различных целей, будут поддерживать дух C требует от них Стандарт или нет, включая принцип «Не мешай программисту от выполнения того, что должно быть сделано». Преобразование указателей в целые числа для выполнения над ними арифметических операций является сообщением для любого компилятора, который намеренно не глух к тому, что пытается сделать программист, т. е. "что нужно сделать".

supercat 30.01.2019 08:20

@Ruslan: [И устав, и опубликованные документы «Обоснование» описывают дух C, хотя сам стандарт его игнорирует].

supercat 30.01.2019 08:21

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

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

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