Подтвердить что ты не робот

Всегда ли безопасно преобразовывать целочисленное значение в void * и обратно в POSIX?

Этот вопрос почти дублирует некоторые другие, которые я нашел, но это касается POSIX и очень распространенного примера в pthreads, с которым я сталкивался несколько раз. Меня в основном интересует текущее состояние дел (т.е. C99 и POSIX.1-2008 или новее), но любая интересная историческая информация также интересна.

Вопрос в основном сводится к тому, будет ли b всегда принимать то же значение, что и в следующем коде:

long int a = /* some valid value */
void *ptr = (void *)a;
long int b = (long int)ptr;

Я знаю, что это обычно работает, но вопрос в том, правильно ли это делать (то есть, стандарты C99 и/или POSIX гарантируют, что он будет работать).

Когда дело доходит до C99, похоже, это не так, у нас есть 6.3.2.3:

5 Целое число может быть преобразовано в любой тип указателя. Кроме того, ранее специфицированный, результат определяется с помощью реализации, может и не быть правильно выровненный, может не указывать на объект ссылочной тип и может быть ловушечным представлением .56)

6 Любой тип указателя может быть преобразованный в целочисленный тип. За исключением случаев, оговоренных ранее результат определяется реализацией. Если результат не может быть представлен в целочисленном типе поведение не определено. Результат не обязательно в диапазоне значений любого целочисленного типа.

Даже используя intptr_t, стандарт, по-видимому, гарантирует, что любой действительный void * может быть преобразован в intptr_t и обратно, но это не гарантирует, что любой intptr_t может быть преобразован в void * и обратно.

Однако все же возможно, что стандарт POSIX позволяет это.

У меня нет большого желания использовать void * в качестве места хранения для любой переменной (я нахожу ее довольно уродливой, даже если POSIX должен ее допускать), но я чувствую, что мне нужно спросить из-за общего примера использования pthreads_create где аргумент start_routine является целым числом, и он передается как void * и преобразуется в int или long int в функцию start_routine. Например эта man-страница имеет такой пример (см. Ссылку для полного кода):

//Last argument casts int to void *
pthread_create(&tid[i], NULL, sleeping, (void *)SLEEP_TIME);
/* ... */
void * sleeping(void *arg){
    //Casting void * back to int
    int sleep_time = (int)arg;
    /* ... */
}

Я также видел аналогичный пример в учебнике ( "Введение в параллельное программирование" Питера С. Пачеко). Учитывая, что это, кажется, обычный пример, используемый людьми, которые должны знать этот материал намного лучше меня, мне интересно, ошибаюсь ли я, и это действительно безопасная и портативная вещь, которую нужно делать.

4b9b3361

Ответ 1

Как вы говорите, C99 не гарантирует, что любой целочисленный тип может быть преобразован в void* и обратно без потери информации. Это делает аналогичную гарантию для intptr_t и uintptr_t, определенных в <stdint.h>, но эти типы являются необязательными. (Гарантией является то, что void* может быть преобразован в {u,}intptr_t и обратно без потери информации, нет такой гарантии для произвольных целочисленных значений.)

POSIX, похоже, не делает такой гарантии.

Описание POSIX <limits.h> требует int и unsigned int не менее 32 бит. Это превышает требование C99, которое должно быть не менее 16 бит. (На самом деле требования относятся к диапазонам, а не к размерам, но эффект заключается в том, что int и unsigned int должны быть не менее 32 (под POSIX) или 16 (под C99) бит, так как C99 требует двоичного представления. )

В описании POSIX <stdint.h> говорится, что intptr_t и uintptr_t должны быть не менее 16 бит, это же требование, налагаемое стандартом C. Поскольку void* может быть преобразован в intptr_t и обратно без потери информации, это означает, что void* может быть как 16 бит. Объедините это с требованием POSIX, что int составляет не менее 32 бит (а требования POSIX и C, что long составляет не менее 32 бит), и возможно, что void* просто недостаточно большой, чтобы удерживать int или long без потери информации.

Описание POSIX pthread_create() не противоречит этому. Он просто говорит, что arg (void* 4-й аргумент pthread_create()) передается в start_routine(). Предположительно, цель состоит в том, что arg указывает на некоторые данные, которые может использовать start_routine(). В POSIX нет примеров, показывающих использование arg.

Здесь вы можете увидеть стандарт POSIX здесь; вам нужно создать бесплатную учетную запись для доступа к ней.

Ответ 2

Фокус в ответах пока что находится на ширине указателя, и, как указывает @Nico (и @Quantumboredom также указывает в комментарии), существует вероятность того, что intptr_t может быть шире, чем указатель. @Kevin отвечает на другую важную проблему, но не полностью ее описывает.

Кроме того, хотя я не уверен в точном абзаце в стандарте, Харбисон и Стил указывают, что intptr_t и uintptr_t также являются необязательными типами и могут даже отсутствовать в действительной реализации C99. OpenGroup говорит, что XSI-совместимые системы должны поддерживать оба типа, но это означает, что простая POSIX поэтому не требует их (по крайней мере, начиная с версии 2003 года).

Часть, которая действительно пропустила здесь, состоит в том, что указатели не всегда должны иметь простое числовое представление, которое соответствует внутреннему представлению целого числа. Это всегда было так (начиная с K & R 1978), и я уверен, что POSIX осторожна, чтобы не отменить эту возможность.

Итак, C99 требует, чтобы можно было преобразовать указатель в intptr_t IFF, который существует, и затем вернуться к указателю снова, чтобы новый указатель все равно указывал на тот же объект в памяти, что и старый указатель, и действительно, если указатели имеют нецелое представление, это означает, что существует алгоритм, который может преобразовать определенный набор целых значений в действительные указатели. Однако это также означает, что не все целые числа между INTPTR_MIN и INTPTR_MAX обязательно являются допустимыми значениями указателя, даже если ширина intptr_t (и/или uintptr_t) точно такая же, как ширина указателя.

Таким образом, стандарты не могут гарантировать, что любые intptr_t или uintptr_t могут быть преобразованы в указатель и обратно к одному и тому же целочисленному значению или даже какой набор целых значений может пережить такое преобразование, поскольку они не могут определить все возможных правил и алгоритмов для преобразования целочисленных значений в значения указателя. Выполнение этого даже для всех известных архитектур могло бы все еще препятствовать применению стандарта новым типам архитектур, которые еще предстоит изобрести.

Ответ 3

(u) intptr_t являются только защищенными, чтобы быть достаточно большими, чтобы удерживать указатель, но они также могут быть "большими", поэтому стандарт C99 гарантирует (void *) → (u) intptr_t → (void *), но в другом случае может произойти потеря данных (и считается undefined).

Ответ 4

Не уверен, что вы подразумеваете под "всегда". Это не написано нигде в стандарте, что это нормально, но нет систем, в которых он не работает.

Если ваши целые числа действительно маленькие (например, ограниченные до 16 бит), вы можете сделать это строго соответствующим, объявив:

static const char dummy_base[65535];

а затем передав dummy_base+i в качестве аргумента и восстановив его как i=(char *)start_arg-dummy_base;

Ответ 5

Я думаю, что ваш ответ в тексте, который вы цитировали:

Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не должен находиться в диапазоне значений любого целочисленного типа.

Итак, не обязательно. Скажем, у вас был 64-разрядный long и отбросил его на void* на 32-битной машине. Указатель, вероятно, 32 бит, так что вы потеряете верхние 32 бита или получите INT_MAX назад. Или, возможно, что-то еще полностью (undefined, как говорится в стандарте).