Всегда ли безопасно преобразовывать целочисленное значение в 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;
/* ... */
}
Я также видел аналогичный пример в учебнике ( "Введение в параллельное программирование" Питера С. Пачеко). Учитывая, что это, кажется, обычный пример, используемый людьми, которые должны знать этот материал намного лучше меня, мне интересно, ошибаюсь ли я, и это действительно безопасная и портативная вещь, которую нужно делать.
Ответы
Ответ 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, как говорится в стандарте).