Безопасно ли использовать значения указателя на страже вблизи максимального целочисленного значения? - PullRequest
5 голосов
/ 10 апреля 2019

Я просматривал некоторый код, который в дополнение к нулевым указателям использует некоторые специальные значения, такие как (T*)-1, как правило, в качестве возвращаемых значений в случае сбоя какой-либо функции create.

Если тип, на который указывает тип, является достаточно большим, так что ((T*)-n) + sizeof(T) будет переполнен, что означает, что адрес никогда не может быть выделен для экземпляра типа T, это нормально? Может ли компилятор увидеть что-то вроде if (ptr == (T*)-1), решить, что это невозможно, и оптимизировать его?

1 Ответ

5 голосов
/ 10 апреля 2019

TL; DR : (T*)-1, скорее всего, будет работать так, как задумано на практике, но для обеспечения безопасности, переносимости и проверки на будущее вы должны использовать вместо указателей нулевые указатели.

Я просматривал некоторый код, который в дополнение к нулевым указателям использует некоторые специальные значения, такие как (T*)-1, как правило, в качестве возвращаемых значений при сбое какой-либо функции create.

Фактически, некоторые интерфейсы POSIX, такие как shmat(), ведут себя аналогично, возвращая (void *)-1, чтобы указать на ошибку. Для них это эквивалент многих других стандартных функций, возвращающих значение int -1. Это значение, которое никогда не будет допустимым возвращаемым значением для успешного вызова. Следовательно, это должно работать в каждой реализации, соответствующей POSIX, и я думаю, что другие требования POSIX имеют общий эффект, требующий того же самого для типов указателей, отличных от void *, также.

В более общем смысле, C явно разрешает преобразование целых чисел в указатели без ограничений, но с оговоркой, что

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

(C2011, 6.3.2.3 / 5 ). Основными проблемами такого преобразования являются

  • что результатом (T*)-1 является представление ловушки, и в этом случае описанная вами схема приводит к неопределенному поведению.
  • что результатом (T*)-1 может быть действительный указатель на T, и в этом случае использование его в качестве сторожа небезопасно.

Насколько мне известно, первое из них не является проблемой для любой реализации C, с которой вы, вероятно, столкнетесь. Я думаю, что второе вряд ли будет проблемой для вас на практике, но если вы ориентируетесь на системы, отличные от POSIX, то я менее уверен в этом.

Вы продолжаете спрашивать,

Где указывается тип, достаточно большой, чтобы ((T *) - n) + sizeof (T) будет переполнен, что означает, что адрес никогда не сможет на самом деле быть выделенным для экземпляра типа T, это нормально? Может ли компилятор увидит что-то вроде if (ptr == (T *) - 1), решив, что это невозможно и оптимизировать это?

Это интересный вопрос. Предполагая, что (T*)-1 не создает представление ловушек, применяется это положение:

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

(C2011, 6.5.9 / 6 )

К сожалению, однако, это немного беспорядок.

Хотя стандарт накладывает ограничения на типы операндов указателей выражения ==, он не требует, чтобы их значения были действительными указателями. Чтобы не было никаких сомнений по этому поводу, это необходимо для внутренней согласованности с положениями раздела 6.3.2.3 , в которых указаны результаты сравнений на равенство с использованием нулевых указателей (не ограничивающихся константами нулевых указателей).

Если хотя бы один из операндов x == y является недопустимым указателем, отличным от нулевого указателя, например, мы можем предположить, (T *)-1, то не выполняется ни одна из альтернатив, указанных в 6.5.9 / 6, поэтому выражение должно иметь значение 0. Компилятор может использовать это для оправдания оптимизации теста и ветвления.

Однако на практике реализации часто не соответствуют этому. Вместо этого они исходят из исторического поведения, возможно, оправдывая себя мимолетной ссылкой на адресное пространство в 6.5.9 / 6 или, возможно, придерживаясь либерального взгляда на то, что такое объект. Для реализаций, которые предоставляют плоское представление адресного пространства, это проявляется как ==, оцениваемый с точки зрения того, совпадают ли адреса, которым соответствуют значения указателя, независимо от отношения этих адресов к любому объекту. Такая реализация, как , не должна оптимизировать тест ==, поскольку она не может с уверенностью предположить, что она всегда будет терпеть неудачу.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...