Рассмотрим следующий код:
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?
@curiousguy Нет особых причин не выравнивать границы байтов в Intel, кроме производительности. Если бы вместо int мы использовали struct i5 { int i[5]; };, на практике p1 и p2 не были бы выровнены по sizeof(i5).
Дополнительный вопрос (хотя заданный ранее): Каково обоснование ограничений на арифметику или сравнение указателей?





const ptrdiff_t ptrDiff = p1 - p2;
Это неопределенное поведение. Вычитание между двумя указателями корректно определено только в том случае, если они указывают на элементы одного и того же массива. ([расшир.доп.] ¶5.3).
When two pointer expressions
PandQare subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined asstd::ptrdiff_tin the<cstddef>header ([support.types]).
- If
PandQboth evaluate to null pointer values, the result is 0.- Otherwise, if P and Q point to, respectively, elements
x[i]andx[j]of the same array objectx, the expressionP - Qhas the valuei−j.- Otherwise, the behavior is undefined
И даже если бы был какой-то гипотетический способ получить это значение законным способом, даже это суммирование является незаконным, поскольку даже суммирование указателя + целое число ограничено, чтобы оставаться в границах массива ([расшир.доп.] ¶4.2)
When an expression
Jthat has integral type is added to or subtracted from an expressionPof pointer type, the result has the type ofP.
- If
Pevaluates to a null pointer value andJevaluates to 0, the result is a null pointer value.- Otherwise, if
Ppoints to elementx[i]of an array objectxwith n elements,81 the expressionsP + JandJ + P(whereJhas the valuej) point to the (possibly-hypothetical) elementx[i+j]if0≤i+j≤nand the expressionP - Jpoints to the (possibly-hypothetical) elementx[i−j]if0≤i−j≤n.- Otherwise, the behavior is undefined.
Есть ли причина, по которой стандарт позволяет вам создавать указатель на элемент после конца массива?
@Vaelus Это упрощает написание циклов, которые увеличивают указатель на каждом шаге. Например, в противном случае for (char *x = xs; x < (xs + sizeof(xs)); x++) {...} был бы недопустимым, поскольку он увеличивает значение x после конца своего массива непосредственно перед прерыванием.
@amalloy было бы недопустимым, потому что он увеличивает x после конца своего массива непосредственно перед прерыванием Это станет незаконным до первого приращения — в xs + sizeof(xs).
@LanguageLawyer Но это явно разрешено, или я неправильно понимаю? Вы можете указать на гипотетический элемент массива, следующий за концом (при условии, что вы не разыменовываете его), поэтому разрешены как xs + sizeof(xs), так и x, равные этому значению.
@MaxLanghof: AFAICT LanguageLawyer просто говорит, что _если бы xs + sizeof(xs) был незаконным (НО ЭТО НЕ ТАК), вы бы получили UB даже при первой оценке условия, непосредственно перед увеличением, поскольку именно там подвыражение xs + sizeof(xs) оценивается для первый раз. При этом, как показано выше, создание указателя на элемент «один-последний» явно разрешено (если вы не разыменовываете его) и является общепринятой идиомой.
@MaxLanghof Как было правильно прокомментировано, мы обсуждаем гипотетическую ситуацию, если не разрешено указывать сразу после последнего элемента. @amalloy сказал, что в этом случае приращение после последней итерации станет недействительным, и я исправил, что xs + sizeof(xs) вызовет UB еще до первого приращения.
@LanguageLawyer О да, я как-то полностью пропустил комментарий Вэлуса - это должно было быть что-то очевидное. Извините за неприятности.
@Vaelus: у каждого объекта есть начальный адрес и конечный адрес, причем последний указывает «сразу за» объектом. Оба адреса допустимы, но только начальный адрес можно использовать непосредственно для доступа к объекту (конечный адрес можно использовать для вычисления начального адреса, который затем можно использовать для адресации объекта).
Третья строка — Undefined Behavior, поэтому стандарт разрешает все, что угодно после нее.
Допустимо только вычитание двух указателей, указывающих на (или после) один и тот же массив.
Windows или Linux на самом деле не имеют значения; компиляторы и особенно их оптимизаторы - это то, что ломает вашу программу. Например, оптимизатор может распознать, что p1 и p2 оба указывают на начало int[100], поэтому p1-p2 должен быть равен 0.
Поскольку третья строка — Undefined Behavior, стандарт также разрешает все до :(
Чтобы добавить стандартную цитату:
When two pointer expressions
PandQare subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined asstd::ptrdiff_tin the<cstddef>header ([support.types]).
(5.1) If
PandQboth evaluate to null pointer values, the result is 0.(5.2) Otherwise, if
PandQpoint to, respectively, elementsx[i]andx[j]of the same array objectx, the expressionP - Qhas the valuei−j.(5.3) Otherwise, the behavior is undefined. [ Note: If the value
i−jis not in the range of representable values of typestd::ptrdiff_t, the behavior is undefined. — end note ]
(5.1) не применяется, поскольку указатели не являются nullptr. (5.2) не применяется, потому что указатели не находятся в одном и том же массиве. Итак, у нас осталось (5.3) — UB.
5.2 может применяться, если у вас есть специальный распределитель (я думаю)
@sudorm-rfslash: Опасная территория. Массивы — это объекты, но распределители создают только хранилище, а не объекты. Два массива — это два разных объекта. В промежутке реализация может зарезервировать пространство для собственных служебных данных независимо от используемого распределителя. Обычно реализация хранит количество элементов, которые необходимо уничтожить. (В Стандартах ведутся споры о том, как формально массивы могут увеличиваться поэлементно, но в основном это касается std::vector. new[100] — это одноразовая операция)
@sudorm-rfslash 5.2 не применяется даже для 2 разных подмассивов (подобъектов одного полного объекта) многомерного массива (например, int a[2][3]; &a[1][0] - &a[0][2]; — это UB), и вы хотите, чтобы он применялся в случае, когда 2 полных объекта массива создаются в одном буфере ( например массив unsigned char)...
@MSalters Почему люди в любом случае так настаивают на выполнении арифметических операций с указателями напрямую, просто приведите их к uintptr_t, а затем добавьте и вычтите сколько душе угодно.
@Joker_vD: Это не обязательно имеет смысл. uintptr_t имеет достаточно битов для хранения значения указателя, вот и все.
Отображение @Joker_vD от целых чисел к указателям и наоборот определяется реализацией, и единственное, что гарантируется, это то, что приведение от указателя к целому числу и обратно должно давать одно и то же значение указателя (если есть целочисленный тип подходящего размера). А GCC, например, не гарантирует больше абсолютного минимума, требуемого стандартом, и говорит, что использование целых чисел для обхода арифметических ограничений указателя — это UB gcc.gnu.org/onlinedocs/gcc/….
@LanguageLawyer GCC делает вид, что целые значения из приведения указателей имеют «происхождение», которое, насколько мне известно, является выдуманным утверждением, не основанным ни на чем в стандартном стандарте.
@curiousguy это выдуманное утверждение не противоречит стандарту, поскольку в любом случае это территория UB.
@MSalters Но арифметика указателей в большинстве случаев гарантированно имеет значение меньше. Черт, чтение значения указателя из правильно инициализированной переменной типа указателя может быть UB, чего не происходит с целыми числами.
@Ruslan Что именно имеет УБ? GCC полностью разработал идею о том, что целое число имеет происхождение, подобное указателю. И кстати, происхождение - это концепция, выдуманная комитетом C, не основанная на каком-либо фактическом пункте стандарта, как написано. И происхождение указателя противоречит тому, что указатели фактов являются тривиальными типами. Вы не можете иметь происхождение и по-прежнему утверждать, что значение является функцией представления. Дело в мошенничестве.
Происхождение значения указателя @curiousguy имеет значение только тогда, когда вы выполняете целочисленную арифметику, чтобы нарушить правило о UB в арифметике указателя. Если вы не попытаетесь обратно преобразовать целое число в указатель, UB не получится — вы просто получите целочисленные результаты, которые были бы, если бы вы написали это на ассемблере.
@curiousguy Комитет объяснил свои намерения и, насколько мне известно, >>> формулировку намерений.
@LanguageLawyer Я согласен с тем, что хорошо понятные намерения важнее, чем точные слова, особенно. когда есть согласие по поводу того, что нужно делать. Я согласен с тем, что некоторые сумасшедшие практики не должны поддерживаться намеренно, например, while(int i=rand(); i!=&x; i=rand()); int *p = (int*)i; // same as p=&x, и по этому поводу существует консенсус. Трудно сказать, что должно поддерживаться в языке программирования высокого/низкого уровня; низкоуровневое программирование конфликтует с высокоуровневой оптимизацией. Я не думаю, что C или C++ хороши в сочетании обоих.
@Ruslan: Стандарт не пытается предписывать, чтобы компиляторы подходили для какой-либо конкретной цели, но вместо этого ожидает, что качественные компиляторы, утверждающие, что они подходят для различных целей, будут поддерживать дух C требует от них Стандарт или нет, включая принцип «Не мешай программисту от выполнения того, что должно быть сделано». Преобразование указателей в целые числа для выполнения над ними арифметических операций является сообщением для любого компилятора, который намеренно не глух к тому, что пытается сделать программист, т. е. "что нужно сделать".
@Ruslan: [И устав, и опубликованные документы «Обоснование» описывают дух C, хотя сам стандарт его игнорирует].
Стандарт допускает реализации на платформах, где память разделена на дискретные области, до которых нельзя добраться друг от друга с помощью арифметики указателей. Простой пример: некоторые платформы используют 24-битные адреса, состоящие из 8-битного номера банка и 16-битного адреса внутри банка. Добавление единицы к адресу, который идентифицирует последний байт банка, даст указатель на первый байт этого банка такой же, а не на первый байт банка следующий. Этот подход позволяет вычислять адресную арифметику и смещения с использованием 16-битной математики, а не 24-битной математики, но требует, чтобы ни один объект не пересекал границу банка. Такой дизайн наложил бы дополнительную сложность на malloc и, вероятно, привел бы к большей фрагментации памяти, чем это могло бы произойти в противном случае, но пользовательскому коду, как правило, не нужно было бы заботиться о разбиении памяти на банки.
Многие платформы не имеют таких архитектурных ограничений, и некоторые компиляторы, предназначенные для низкоуровневого программирования на таких платформах, позволяют выполнять адресную арифметику между произвольными указателями. Стандарт отмечает, что общий способ обработки неопределенного поведения - это «поведение во время трансляции или выполнения программы в задокументированной манере, характерной для среды», и поддержка обобщенной арифметики указателей в средах, которые ее поддерживают, хорошо вписывается в эту категорию. К сожалению, Стандарт не предоставляет каких-либо средств для различения реализаций, которые ведут себя таким полезным образом, и тех, которые этого не делают.
Нет даже гарантии, что объекты
intвыровнены поsizeof(int)(это касается всех известных мне ABI, но почти во всех правилах программирования есть исключения, поэтому некоторые ABI могут быть не такими); когда это не так, код, очевидно, не может гарантировать работу.