Чи має еквівалент std :: менше від C ++?


26

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

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


1
Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

Відповіді:


20

На реалізаціях з плоскою моделлю пам'яті (в основному все), передавайте на uintptr_tWill Just Work.

(Але дивіться Чи слід порівняння покажчиків підписувати чи не підписати в 64-бітному x86? Для обговорення того, чи слід ставитися до покажчиків як до підписаних чи ні, включаючи питання формування покажчиків поза об'єктами, які є UB у C.)

Але системи з неплоскими моделями пам'яті існує, і думати про них можуть допомогти пояснити поточну ситуацію, як C ++ , що мають різні функції для <VS. std::less.


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

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

C ++ std::lessу покажчиках на дивних ISA може бути дорогим , наприклад, "нормалізувати" сегмент: зміщення на x86-16, щоб змістити <= 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-бітна "компенсована" частина використовується як покажчик .)

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


Чому це було б UB, якщо сегмент не дорівнює?
Жолудь

@Acorn: Хочеться сказати, що навпаки; фіксований. покажчики на один і той же об’єкт матимуть той самий сегмент, інакше UB.
Пітер Кордес

Але чому ви вважаєте, що це UB у будь-якому випадку? (перевернута логіка чи ні, насправді я також не помітив)
Жолудь

p < qє UB в C, якщо вони вказують на різні об'єкти, чи не так? Я знаю, що p - qце.
Пітер Кордес

1
@Acorn: У всякому разі, я не бачу механізму, який би генерував псевдоніми (різні сегменти: вимкнено, однакова лінійна адреса) в програмі без UB. Отже, це не так, як компілятор повинен вийти зі свого шляху, щоб уникнути цього; при кожному доступі до об'єкта використовується значення цього об'єкта segі зміщення, яке> = зміщення в сегменті, з якого починається цей об'єкт. C змушує UB робити велику кількість між покажчиками на різні об'єкти, включаючи такі речі, як tmp = a-bі потім b[tmp]для доступу a[0]. Ця дискусія про сегментування сегментованого вказівника є хорошим прикладом того, чому вибір дизайну має сенс.
Пітер Кордес

17

Одного разу я намагався знайти шлях до цього, і я знайшов рішення, яке працює для об'єктів, що перекриваються, і в більшості інших випадків припускаючи, що компілятор робить "звичайну" річ.

Спочатку ви можете реалізувати пропозицію в розділі Як реалізувати memmove у стандартному C без проміжної копії? а потім, якщо це не працює для передачі uintptr(тип обгортки для будь-якого uintptr_tабо unsigned long longзалежно від того, чи uintptr_tє він), і отримаєте найімовірніший точний результат (хоча це, мабуть, не матиме значення):

#include <stdint.h>
#ifndef UINTPTR_MAX
typedef unsigned long long uintptr;
#else
typedef uintptr_t uintptr;
#endif

int pcmp(const void *p1, const void *p2, size_t len)
{
    const unsigned char *s1 = p1;
    const unsigned char *s2 = p2;
    size_t l;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s1 + l == s2 || s1 + l == s2 + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s1 > s2)
                return 1;
            else if (s1 < s2)
                return -1;
            else
                return 0;
        }
    }

    /* No overlap so the result probably won't really matter.
       Cast the result to `uintptr` and hope the compiler
       does the "usual" thing */
    if((uintptr)s1 > (uintptr)s2)
        return 1;
    else if ((uintptr)s1 < (uintptr)s2)
        return -1;
    else
        return 0;
}

5

Чи пропонує 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 
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.