Реализация эквивалента std :: bit_cast в C
Можно ли реализовать что-то похожее на C ++ 20? std::bit_cast
в C? Это было бы намного удобнее, чем использовать
union
или приведение указателей к разным типам и разыменование.
Если бы у вас был, тогда было бы проще реализовать некоторые функции с плавающей запятой:
float Q_rsqrt( float number )
{
int i = 0x5f3759df - ( bit_cast(int, number) >> 1 );
float y = bit_cast(float, i);
y = y * ( 1.5f - ( number * 0.5f * y * y ) );
y = y * ( 1.5f - ( number * 0.5f * y * y ) );
return y;
}
См. Также Быстрый обратный квадратный корень
Наивное решение:
#define bit_cast(T, ...) (*(T*) &(__VA_ARGS__))
Но у него есть серьезные проблемы:
- это неопределенное поведение, потому что оно нарушает строгий псевдоним
- он не работает для rvalue с побитовым преобразованием, потому что мы напрямую берем адрес второго операнда
- он не гарантирует, что операнды имеют одинаковый размер
Можем ли мы реализовать
bit_cast
без этих проблем?
1 ответ
Это возможно в нестандартном стандарте C, благодаря . также является еще одной предлагаемой функцией для C23 , поэтому она может стать возможной в стандарте C23. Одно из приведенных ниже решений приносит некоторые жертвы, которые обеспечивают соответствие C99.
Реализация с использованием
Давайте посмотрим, как подход с использованием
union
работает сначала:
#define bit_cast(T, ...) \
((union{typeof(T) a; typeof(__VA_ARGS__) b;}) {.b=(__VA_ARGS__)}.a)
Мы создаем составной литерал из анонимного объединения, состоящего из любого типа данного выражения. Мы инициализируем этот литерал для
.b= ...
используя назначенные инициализаторы, а затем получить доступ к
.a
член типа .
The
typeof(T)
необходимо, если мы хотим каламбурить указатели функций, массивы и т. д. из-за синтаксиса типа C.
Реализация с использованием
Эта реализация немного длиннее, но имеет то преимущество, что она полагается только на C99 и может работать даже без использования :
#define bit_cast(T, ...) \
(*(typeof(T)*) memcpy(&(T){0}, &(typeof(__VA_ARGS__)) {(__VA_ARGS__)}, sizeof(T)))
Мы копируем из одного составного литерала в другой, а затем получаем доступ к целевому значению:
- исходный литерал является копией нашего входного выражения, что позволяет нам брать его адрес даже для
bit_cast(float, 123)
куда123
является rvalue - пункт назначения является инициализированным нулем литералом типа
T
memcpy
возвращает операнд назначения, поэтому мы можем привести результат к
typeof(T)*
а затем разыменовать этот указатель.
Мы можем полностью отказаться от этого и сделать его совместимым с C99, но есть недостатки:
#define bit_cast(T, ...) \
(*((T*) memcpy(&(T){0}, &(__VA_ARGS__), sizeof(T))))
Теперь мы берем адрес выражения напрямую, поэтому мы не можем использовать
bit_cast
на rvalues больше. мы используем
T*
без
typeof
, поэтому мы больше не можем конвертировать в указатели на функции, массивы и т. д.
Внедрение проверки размера (начиная с C11)
Что касается последней проблемы, заключающейся в том, что мы не проверяем, что оба операнда имеют одинаковый размер: мы можем использовать (начиная с C11), чтобы убедиться в этом. К сожалению,
_Static_assert
является объявлением, а не выражением, поэтому мы должны его обернуть:
#define static_assert_expr(...) \
((void) (struct{_Static_assert(__VA_ARGS__); int _;}) {0})
Мы создаем составной литерал, который содержит утверждение и отбрасывает выражение.
Мы можем легко интегрировать это в предыдущие две реализации, используя оператор запятой:
#define bit_cast_memcpy(T, ...) ( \
static_assert_expr(sizeof(T) == sizeof(__VA_ARGS__), "operands must have the same size"), \
(*(typeof(T)*) memcpy(&(T){0}, &(typeof(__VA_ARGS__)) {(__VA_ARGS__)}, sizeof(T))) \
)
#define bit_cast_union(T, ...) ( \
static_assert_expr(sizeof(T) == sizeof(__VA_ARGS__), "operands must have the same size"), \
((union{typeof(T) a; typeof(__VA_ARGS__) b;}) {.b=(__VA_ARGS__)}.a) \
)
Известные и неустранимые проблемы
Из-за того, как работают макросы, мы не можем использовать это, если каламбурный тип содержит запятую:
bit_cast(int[0,1], x)
Это не работает, потому что макросы игнорируют квадратные скобки и
1]
не будет считаться частью типа, но войдет в
__VA_ARGS__
.