Имеет ли C эквивалент std::less из C++?

Недавно я отвечал на вопрос о неопределенном поведении выполнения p < q в C, когда p и q являются указателями на разные объекты/массивы. Это заставило меня задуматься: C++ имеет такое же (неопределенное) поведение < в этом случае, но также предлагает стандартный шаблон библиотеки std::less, который гарантированно возвращает то же самое, что и <, когда указатели могут быть сравнивать и возвращать последовательный порядок, когда они не могут.

Предлагает ли C что-то с аналогичной функциональностью, которая позволила бы безопасно сравнивать произвольные указатели (с тем же типом)? Я попытался просмотреть стандарт C11 и ничего не нашел, но мой опыт в C на порядки меньше, чем в C++, поэтому я мог легко что-то упустить.

Ответы

Ответ 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-битное число.

Ответ 2

Нет. Я однажды пытался найти способ обойти это, но ничего не смог найти.

Ваша лучшая ставка - это, вероятно, приведение к uintptr_t и надежда на то, что компилятор поступит правильно, как я в итоге сделал:

void *memmove(void *dest, const void *src, size_t len)
{
    const unsigned char *s = (const unsigned char *)src;
    unsigned char *d = (unsigned char *)dest;

    /* The most portable this is ever going to get
     * without incurring an O(n) memory penalty
     */
    if((uintptr_t)dest < (uintptr_t)(void *)src)
    {
...

Ответ 3

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

Нет


Сначала давайте рассмотрим только объектные указатели. Указатели на функции вызывают целый ряд других проблем.

2 указателя p1, p2 могут иметь разные кодировки и указывать на один и тот же адрес, поэтому p1 == p2, хотя memcmp(&p1, &p2, sizeof p1) не равно 0. Такие архитектуры встречаются редко.

Тем не менее, преобразование этих указателей в uintptr_t не требует того же самого целочисленного результата, приводящего к (uintptr_t)p1 != (uinptr_t)p2.

(uintptr_t)p1 < (uinptr_t)p2 сам по себе является вполне законным кодом, поскольку может не обеспечивать ожидаемую функциональность.


Если код действительно должен сравнивать несвязанные указатели, создайте вспомогательную функцию less(const void *p1, const void *p2) и выполните там код, специфичный для платформы.

Возможно:

// return -1,0,1 for <,==,> 
int ptrcmp(const void *c1, const void *c1) {
  // Equivalence test works on all platforms
  if (c1 == c2) {
    return 0;
  }
  // At this point, we know pointers are not equivalent.
  #ifdef UINTPTR_MAX
    uintptr_t u1 = (uintptr_t)c1;
    uintptr_t u2 = (uintptr_t)c2;
    // Below code "works" in that the computation is legal,
    //   but does it function as desired?
    // Likely, but strange systems lurk out in the wild. 
    // Check implementation before using
    #if tbd
      return (u1 > u2) - (u1 < u2);
    #else
      #error TBD code
    #endif
  #else
    #error TBD code
  #endif 
}