Обоснование для сравнения указателей вне массива должно быть UB

Итак, стандарт (со ссылкой на N1570 ) говорит о сравнении указателей:

C99 6.5.8 / 5 Реляционные операторы

Когда сравниваются два указателя, результат зависит от относительных местоположений в адресном пространстве объектов, на которые указывает. … [snip очевидные определения сравнения в совокупности] … Во всех остальных случаях поведение не определено.

Каково обоснование этого экземпляра UB, в отличие от указания (например) преобразования в intptr_t и сравнения этого?

Есть ли какая-то машинная архитектура, где сложно сконструировать разумный общий порядок указателей? Существует ли какой-либо class оптимизации или анализа, который может препятствовать неограниченному сравнению указателей?

В удаленном ответе на этот вопрос упоминается, что этот кусок UB позволяет пропускать сравнение регистров сегментов и сравнивать только смещения. Это особенно ценно для сохранения?

(Тот же удаленный ответ, а также один здесь, обратите внимание, что в C ++ std::less и т. Д. Необходимы для реализации общего порядка указателей, независимо от того, выполняет ли оператор нормального сравнения).

Различные комментарии в обсуждении списка рассылки ub Обоснование <<не полный порядок указателей? в качестве аргумента сильно ссылаются на сегментированные архитектуры. Включая следующие комментарии, 1 :

Отдельно я считаю, что основной язык должен просто признать тот факт, что все машины в эти дни имеют модель с плоской памятью.

и 2 :

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

и 3 :

Указатели, в то время как исторически не полностью упорядоченные, практически для всех существующих в настоящее время систем, за исключением соображений башни из слоновой кости комитета, так что вопрос спорный.

и 4 :

Но даже если сегментированные архитектуры, маловероятные, хотя они и есть, вернутся, проблема упорядочения еще должна быть решена, поскольку std :: less требуется для полного указания указателей. Я просто хочу, чтобы оператор <был альтернативным написанием для этого свойства.

Почему все остальные притворяются страшными (и я имею в виду притворство, потому что за пределами небольшого контингента комитета люди уже предполагают, что указатели полностью упорядочены относительно оператора <), чтобы удовлетворить теоретические потребности какой-то в настоящее время несуществующей архитектуры ?

Отвечая на тенденцию комментариев из списка рассылки ub , FUZxxl указывает, что поддержка DOS является причиной не поддержки полностью упорядоченных указателей.

Обновить

Это также поддерживается справочным руководством Annotated C ++ ( ARM ), в котором говорится, что это связано с бременем поддержки этого на сегментированных архитектурах:

Выражение может не оцениваться как false на сегментированных архитектурах […] Это объясняет, почему сложение, вычитание и сравнение указателей определяются только для указателей в массив и один элемент за его пределами. […] Пользователи машин с несегментированным адресным пространством разработали идиомы, однако, что ссылки на элементы за пределами массива […] не были переносимы на сегментированные архитектуры, если не были предприняты особые усилия […] Разрешение […] было бы дорогостоящим и послужило бы нескольким полезным целям.

8086 – это процессор с 16-разрядными регистрами и 20-битовое адресное пространство. Чтобы справиться с отсутствием битов в своих регистрах, существует набор регистров сегментов . При доступе к памяти разыменованный адрес вычисляется следующим образом:

 address = 16 * segment + register 

Обратите внимание, что, помимо прочего, адрес имеет, как правило, несколько способов представления. Сравнение двух произвольных адресов утомительно, поскольку компилятор должен сначала нормализовать оба адреса, а затем сравнить нормализованные адреса.

Многие компиляторы указывают (в моделях памяти, где это возможно), что при выполнении арифметики указателя часть сегмента остается нетронутой. Это имеет несколько последствий:

  • объекты могут иметь размер не более 64 КБ
  • все адреса в объекте имеют одну и ту же часть сегмента
  • сравнение адресов в объекте может быть выполнено только путем сравнения регистрационной части; что можно сделать в одной инструкции

Это быстрое сравнение, конечно, работает только тогда, когда указатели производятся из одного и того же базового адреса, что является одной из причин, почему стандарт C определяет сравнения указателей только тогда, когда оба указателя указывают на один и тот же объект.

Если вы хотите хорошо упорядоченное сравнение для всех указателей, сначала подумайте о преобразовании указателей в значения uintptr_t .

Я считаю, что это не определено, так что C можно запускать на архитектурах, где, по сути, «интеллектуальные указатели» реализованы на аппаратном обеспечении, с различными проверками, чтобы гарантировать, что указатели никогда не указывают случайно за пределами областей памяти, на которые они определены. Я никогда лично не использовал такую ​​машину, но думать о ней – это то, что вычисление недопустимого указателя точно так же запрещено, как деление на 0; вы, вероятно, получите исключение во время выполнения, которое завершает вашу программу. Кроме того, запрещается вычислять указатель, вам даже не нужно разыгрывать его, чтобы получить исключение.

Да, я считаю, что это определение также позволило более эффективно сравнивать регистры смещения в старом коде 8086, но это была не единственная причина.

Да, компилятор для одной из этих защищенных архитектур-указателей теоретически мог бы реализовать «запрещенные» сравнения путем преобразования в беззнаковый или эквивалентный, но (а) он, вероятно, был бы значительно менее эффективным для этого, и (б) это было бы бессмысленно преднамеренное обход предполагаемой защиты архитектуры, защита, по которой, по крайней мере, некоторые программисты из C, по-видимому, захотят включить (не отключить).

Исторически, говоря, что действие, вызванное Undefined Behavior, означало, что любая программа, которая использовала такие действия, могла ожидать, что она будет корректно только для тех реализаций, которые определяют для этого действия поведение, соответствующее их требованиям . Указание того, что действие, вызванное Undefined Behavior, не означает, что программы, использующие такие действия, следует считать «незаконными», но скорее предназначались для того, чтобы позволить C использовать для запуска программ, которые не требовали таких действий, на платформах, которые не могли эффективно поддерживать их.

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

Как отмечали другие, существуют некоторые платформы, на которых p1 < p2 , для несвязанных указателей p1 и p2, может быть гарантировано получить 0 или 1, но где наиболее эффективное средство сравнения p1 и p2, которое будет работать в случаях, Стандарт не может отстоять обычное ожидание, что p1 < p2 || p2 > p2 || p1 != p2 p1 < p2 || p2 > p2 || p1 != p2 p1 < p2 || p2 > p2 || p1 != p2 . Если программа, написанная для такой платформы, знает, что она никогда не будет преднамеренно сравнивать несвязанные указатели (подразумевая, что любое такое сравнение будет представлять собой программную ошибку), может быть полезно, чтобы стресс-тестирование или сборщик неисправностей генерировали код, который ловушки при любых таких сравнениях. Единственный способ для Стандарта разрешить такие реализации - сделать такие сравнения Неопределенным Поведением.

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

Если принять понятия, что:

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

  2. Существуют платформы, где сравнение несвязанных указателей будет проблематичным

Затем Стандарт считает, что сравнение несвязанных указателей как Неопределенное Поведение. Если бы они ожидали, что даже компиляторы для платформ, которые определяют непересекающееся глобальное ранжирование для всех указателей, могут сделать сравнение не связанных между собой указателей, отрицают законы времени и причинности (например, данные:

 int needle_in_haystack(char const *hs_base, int hs_size, char *needle) { return needle >= hs_base && needle < hs_base+hs_size; } 

компилятор может сделать вывод о том, что программа никогда не получит никакого ввода, из-за чего для параметра needle_in_haystack будут заданы несвязанные указатели, и любой код, который будет иметь значение только тогда, когда программа получит такой ввод, может быть устранена). Я думаю, что они бы указали разные вещи по-разному. Авторы компилятора, вероятно, утверждают, что правильным способом записи needle_in_haystack было бы:

 int needle_in_haystack(char const *hs_base, int hs_size, char *needle) { for (int i=0; i 

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