Есть ли в C эквивалент std :: less из C ++? - PullRequest
25 голосов
/ 10 октября 2019

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

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

Ответы [ 3 ]

19 голосов
/ 11 октября 2019

В реализациях с режимом плоской памяти (в основном все) приведение к uintptr_t будет просто работать.

(Но см. Должны ли сравнения указателей быть подписанными или неподписанными в 64-битной x86? для обсуждения того, следует ли вам рассматривать указатели как подписанные или нет, включая вопросы формирования указателей вне объектов, что является UB в C.)

Но системы с неплоскими моделями памяти существуют,и размышления о них могут помочь объяснить текущую ситуацию, например, C ++ имеет различные спецификации для < против std::less.


Часть точки < на указателях для разделенияобъекты, являющиеся UB в C (или, по крайней мере, не определенные в некоторых ревизиях C ++), должны учитывать странные машины, включая не плоские модели памяти.

Хорошо известным примером является реальный режим x86-16, где указатели являются сегментными: смещение, формирующее 20-битный линейный адрес через (segment << 4) + offset. Один и тот же линейный адрес может быть представлен несколькими различными комбинациями seg: off.

C ++ std::less для указателей на странных ISA может потребовать больших затрат , например, "нормализовать" сегмент: смещениена x86-16 иметь смещение <= 15. Тем не менее, <em>portable не существует способа реализовать это. Манипуляции, необходимые для нормализации uintptr_t (или объектного представления объекта-указателя), зависят от реализации.

Но даже в системах, где C ++ std::less должен быть дорогим, < не должно быть. Например, предполагая «большую» модель памяти, в которой объект помещается в пределах одного сегмента, < может просто сравнить смещенную часть и даже не беспокоиться с частью сегмента. (Указатели внутри одного и того же объекта будут иметь один и тот же сегмент, и в противном случае это UB в C. C ++ 17 заменен на просто «неопределенный», что может все же позволить пропустить нормализацию и просто сравнить смещения.) Это предполагает, что все указатели на любую частьобъекта всегда использовать то же значение seg, никогда не нормализуя. Это то, что вы ожидаете от ABI для «большой» модели в отличие от «огромной» модели памяти. (См. обсуждение в комментариях ).

(Такая модель памяти может иметь максимальный размер объекта, например, 64 КБ, но гораздо большее максимальное общее адресное пространство, в котором есть место для многих таких max. объекты размера 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-битная часть «смещения» используется в качестве указателя.)

Вы можете вручную добавить код для различных конкретных платформ, например, по умолчанию предположить, что он плоский, или #ifdef что-то для обнаружения реального режима x86 и разделить uintptr_t на 16-битные половины для seg -= off>>4; off &= 0xf;, а затем объединитьэти части возвращаются в 32-битное число.

17 голосов
/ 10 октября 2019

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

Ваша лучшая ставка, вероятно, приведена к 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)
    {
...
5 голосов
/ 10 октября 2019

Предлагает ли 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 
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...