Ответ 1
В реализациях с плоским режимом памяти (в основном все) приведение к uintptr_t
будет просто работать.
Но системы с неплоскими моделями памяти существуют, и размышления о них могут помочь объяснить текущую ситуацию, например, C++ имеет различные спецификации для <
и std::less
.
Часть смысла <
в отношении указателей на отдельные объекты, являющиеся UB в C (или, по крайней мере, не определенные в некоторых ревизиях C++), заключается в том, чтобы учесть странные машины, включая неплоские модели памяти.
Хорошо известным примером является реальный режим x86-16, где указатели являются сегментами: смещение, образуя 20-битный линейный адрес через (segment << 4) + offset
. Один и тот же линейный адрес может быть представлен несколькими различными комбинациями seg: off.
C++ std::less
на указателях на странных ISA, возможно, должны быть дорогими, например. "нормализовать" сегмент: смещение на x86-16, чтобы иметь смещение & lt; = 15. Однако нет портативного способа реализовать это. Манипуляции, необходимые для нормализации uintptr_t
(или объектного представления объекта указателя), зависят от реализации.
Но даже в системах, где C++ std::less
должен быть дорогим, <
не должен быть. Например, предполагая "большую" модель памяти, в которой объект помещается в пределах одного сегмента, <
может просто сравнить смещенную часть и даже не беспокоиться с частью сегмента. (Указатели внутри одного и того же объекта будут иметь один и тот же сегмент, и в противном случае это UB в C. C++ 17 будет изменено на просто "неопределенное", что может все же позволить пропустить нормализацию и просто сравнить смещения.) Это предполагает, что все указатели на любую часть объекта всегда использовать одно и то же значение seg
, никогда не нормализуя. Это то, что вы ожидаете от ABI для "большой" модели в отличие от "огромной" модели памяти. (См. обсуждение в комментариях).
(Такая модель памяти может иметь максимальный размер объекта, например, 64 КБ, но гораздо большее максимальное общее адресное пространство, в котором есть место для многих таких объектов максимального размера. ISO C позволяет реализациям иметь ограничение на размер объекта, которое меньше, чем Максимальное значение (без знака) size_t
может представлять SIZE_MAX
. Например, даже в системах с плоской памятью GNU C ограничивает максимальный размер объекта до PTRDIFF_MAX
, поэтому при расчете размера можно игнорировать переполнение со знаком.) См. этот ответ и обсуждение в комментариях.
Если вы хотите разрешить объекты размером больше сегмента, вам нужна "огромная" модель памяти, которая должна беспокоиться о переполнении смещенной части указателя при выполнении p++
для циклического перемещения по массиву или при выполнении арифметики индексирования/указателя. Это повсеместно приводит к более медленному коду, но, вероятно, будет означать, что p < q
будет работать для указателей на разные объекты, потому что реализация, нацеленная на "огромную" модель памяти, обычно предпочитает постоянно поддерживать все указатели нормализованными. См. Что такое ближний, дальний и огромный указатели? - некоторые реальные компиляторы C для реального режима x86 имели возможность компилировать для "огромной" модели, где все указатели по умолчанию установлены в "огромный", если не указано иное.
Сегментация реального режима x86 - не единственная возможная модель неплоской памяти, это всего лишь полезный конкретный пример, иллюстрирующий, как она обрабатывается реализациями C/C++. В реальной жизни реализации расширили ISO C концепцией указателей far
и near
, что позволяет программистам выбирать, когда им удастся просто сохранить/передать 16-битную часть смещения относительно некоторого общего сегмента данных.
Но для реализации в чистом ISO C придется выбирать между маленькой моделью памяти (все, кроме кода в том же 64-килобайтном формате с 16-разрядными указателями) или большой или огромной, причем все указатели являются 32-разрядными. Некоторые циклы можно оптимизировать, увеличивая только смещенную часть, но объекты указателя нельзя оптимизировать, чтобы они были меньше.
Если вы знали, что такое магическая манипуляция для какой-либо конкретной реализации, вы могли бы реализовать ее на чистом C. Проблема в том, что разные системы используют разные адресации, а детали не параметризуются никакими переносимыми макросами.
Или, может быть, нет: это может включать поиск чего-то из специальной таблицы сегментов или что-то, например, как в защищенном режиме x86, а не в реальном режиме, где сегментная часть адреса является индексом, а не значением, смещаемым влево. Вы можете установить частично перекрывающиеся сегменты в защищенном режиме, и части адресов сегмента селектора не обязательно будут упорядочены в том же порядке, что и соответствующие базовые адреса сегментов. Для получения линейного адреса из указателя seg: off в защищенном режиме x86 может потребоваться системный вызов, если GDT и/или LDT не отображаются на читаемые страницы в вашем процессе.
(Конечно, основные операционные системы для x86 используют плоскую модель памяти, поэтому база сегмента всегда равна 0 (за исключением локального хранилища потока с использованием сегментов fs
или gs
), и только 32-битное или 64-битное "смещение" часть используется как указатель.)
Вы можете вручную добавить код для различных конкретных платформ, например, по умолчанию, предположим, что flat или #ifdef
что-то обнаруживают в реальном режиме x86 и разбивают uintptr_t
на 16-битные половины для seg -= off>>4; off &= 0xf;
, а затем объединяют эти части обратно в 32-битное число.