Всегда ли безопасно преобразовать целочисленное значение в void * и обратно в POSIX? - PullRequest
23 голосов
/ 19 октября 2011

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

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

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 позволяет это.

У меня нет особого желания использовать пустоту * в качестве места для хранения любой переменной (я нахожу это довольно уродливым, даже если 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;
    /* ... */
}

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

Ответы [ 5 ]

13 голосов
/ 19 октября 2011

Как вы говорите, 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 здесь ; Вы должны создать бесплатный аккаунт для доступа к нему.

6 голосов
/ 14 ноября 2012

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

Кроме того, хотя я не уверен в точном абзаце в стандарте, Харбисон и Стил указывают, что 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 голосов
/ 19 октября 2011

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

2 голосов
/ 19 октября 2011

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

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

static const char dummy_base[65535];

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

1 голос
/ 19 октября 2011

Я думаю, что ваш ответ содержится в цитируемом вами тексте:

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

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

...