Безопасно ли хранить байт в кросс-платформенном указателе void?

void *vp = (void *)5;
uint8_t i = (uint8_t)vp;

Будет ли i == 5 на всех 8-битных и выше процессорах? Чем это чревато? Есть ли лучший способ хранить в переменной 8-битный целочисленный литерал или указатель в C99?

У меня есть массив указателей функций на функции, которые принимают void *. Некоторые функции должны интерпретировать void * как uint8_t.


person jestro    schedule 30.06.2021    source источник
comment
Почему вы хотите это сделать?   -  person dbush    schedule 01.07.2021
comment
Правильный способ сделать это — использовать объединение, содержащее void * и uint8_t. Вот для чего нужны профсоюзы.   -  person user3386109    schedule 01.07.2021
comment
Если вы хотите сохранить его в целочисленном типе, используйте uintptr_t.   -  person Barmar    schedule 01.07.2021
comment
warning: initialization of ‘void *’ from ‘int’ makes pointer from integer without a cast так говорит GCC, и это без явного включения каких-либо предупреждений. Также, согласно комментарию @dbush, не могли бы вы немного расширить свой вопрос, поскольку неясно, что вы пытаетесь сделать и почему.   -  person Rodney    schedule 01.07.2021
comment
Если вы пытаетесь передать это значение в качестве параметра функции потока, это не так.   -  person dbush    schedule 01.07.2021
comment
Как функция узнает, является ли аргумент void * или uint8_t? Или, другими словами, что такое сигнатура функции?   -  person user3386109    schedule 01.07.2021
comment
@ user3386109: Контекст. Использование void * в качестве типа определяемого пользователем параметра обратного вызова является очень распространенной идиомой. Вы зарегистрировали функцию обратного вызова, передав ее как определяемую пользователем. параметр, поэтому функция обратного вызова знает, что с ним делать при вызове с ним. Я считаю, что причина в том, что void * достаточно велик, чтобы содержать любой тип указателя, в который вы хотите его применить.   -  person marko    schedule 01.07.2021
comment
Улучшенное объяснение.   -  person jestro    schedule 01.07.2021
comment
@marko Да, это нормально, если все обратные вызовы принимают указатель в качестве аргумента и используют указатель в качестве указателя. Я не видел обратных вызовов, где параметр является либо указателем, либо каким-то другим произвольным типом. Это не стандартная идиома, она более известна как спагетти-код.   -  person user3386109    schedule 01.07.2021


Ответы (2)


void *vp = 5; не должен компилироваться; стандарт C по крайней мере требует, чтобы компилятор выдавал диагностическое сообщение. Вы можете запросить преобразование с помощью void *vp = (void *) 5;, и вы можете запросить обратное преобразование с помощью (uint8_t) vp. Стандарт C не гарантирует, что это воспроизведет исходное значение. (Преобразования с использованием указателей указаны в C 2018 6.3.2.3.) Скорее всего, это будет работать в большинстве реализаций C.

Альтернативой, которая была бы определена стандартом C, было бы использование смещений в какой-нибудь достаточно большой объект, который у вас уже есть. Например, если у вас есть массив A, и вы хотите сохранить небольшое число n в void *, вы можете сделать:

void *vp = (char *) A + n; // Point n bytes into the object A.

и вы можете восстановить номер с помощью:

(char *) vp - (char *) A // Subtract base address to recover offset.
person Eric Postpischil    schedule 30.06.2021
comment
Обновленный вопрос - person jestro; 01.07.2021
comment
Я подозреваю, что условие «работает в большинстве реализаций» заключается в том, что ЦП способен хранить как целочисленный тип, так и адрес в регистре общего назначения без необходимости использовать память для преобразования - в этом случае порядок следования байтов может вас укусить. Вероятно, вам придется сильно постараться, чтобы найти тот, где он не работает. - person marko; 01.07.2021

Стандарт C разрешает преобразования между целым числом и указателем, однако не объясняет, как именно это должно происходить. Это зависит от каждой конкретной реализации.

Раздел 6.3.2.3 p5-6 стандарта C описывает эти преобразования:

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

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

В частности, в gcc то, что вы делаете, будет работать, однако это не гарантирует работу на всех компиляторах или архитектурах.

Что гарантированно работает, так это получение адреса составного литерала:

void *vp = &(uint8_t){5};
uint8_t i = *(uint8_t *)vp;

Это создает временный объект типа uint8_t и получает его адрес. Затем этот адрес можно преобразовать в void * и обратно, что полностью соответствует стандарту, как указано в параграфе 1 раздела 6.3.2.3:

Указатель на void может быть преобразован в или из указателя на объект любого типа. Указатель на объект любого типа может быть преобразован в указатель на void и обратно; результат будет равен исходному указателю.

Время жизни составного литерала равно времени жизни блока, в котором он определен. Поэтому, пока указатель не используется после окончания этого блока, он будет работать.

Однако, если вы собираетесь передать его функции, которая запускает поток, вам лучше динамически выделять память для значения и передавать его. В противном случае вы рискуете, что функция, в которой определен составной литерал, вернется во время выполнения функции потока, которая может попытаться использовать этот указатель.

person dbush    schedule 01.07.2021
comment
В частности, под gcc то, что вы делаете, будет работать За исключением того, что документ GCC говорит, что это UB. - person Language Lawyer; 02.07.2021
comment
@LanguageLawyer Это не так. См. эту страницу. - person dbush; 02.07.2021