Implementing std::bit_cast equivalent in C

307 Views Asked by At

Is it possible to implement something similar to C++20's std::bit_cast in C? It would be a lot more convenient than using union or casting pointers to different types and dereferencing.

If you had a bit_cast, then implementing some floating point functions would be easier:

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;
}

See also Fast inverse square root

The naive solution is:

#define bit_cast(T, ...) (*(T*) &(__VA_ARGS__))

But it has major problems:

  1. it is undefined behavior because it violates strict aliasing
  2. it doesn't work for bit-casting rvalues because we are taking the address of the second operand directly
  3. it doesn't make sure that the operands have the same size

Can we implement a bit_cast without these issues?

1

There are 1 best solutions below

2
On BEST ANSWER

It is possible in non-standard standard C, thanks to typeof. typeof is also a further proposed feature for C23, so it may become possible in standard C23. One of the solutions below makes some sacrifices which allow C99 compliance.

Implementation Using union

Let's look at how the approach using union works first:

#define bit_cast(T, ...) \
  ((union{typeof(T) a; typeof(__VA_ARGS__) b;}) {.b=(__VA_ARGS__)}.a)

We are creating a compound literal from an anonymous union made of T and whatever type the given expression has. We initialize this literal to .b= ... using designated initializers and then access the .a member of type T.

The typeof(T) is necessary if we want to pun function pointers, arrays, etc., due to C's type syntax.

Implementation using memcpy

This implementation is slightly longer, but has the advantage of relying only on C99, and can even work without the use of typeof:

#define bit_cast(T, ...) \
    (*(typeof(T)*) memcpy(&(T){0}, &(typeof(__VA_ARGS__)) {(__VA_ARGS__)}, sizeof(T)))

We are copying from one compound literal to another and then accessing the destination's value:

  • the source literal is a copy of our input expression, which allows us to take its address, even for bit_cast(float, 123) where 123 is an rvalue
  • the destination is a zero-initialized literal of type T

memcpy returns the destination operand, so we can cast the result to typeof(T)* and then dereference that pointer.

We can completely eliminate typeof here and make this C99-compliant, but there are downsides:

#define bit_cast(T, ...) \
    (*((T*) memcpy(&(T){0}, &(__VA_ARGS__), sizeof(T))))

We are now taking the address of the expression directly, so we can't use bit_cast on rvalues anymore. We are using T* without typeof, so we can no longer convert to function pointers, arrays, etc.

Implementing Size Checking (since C11)

As for the last issue, which is that we don't verify that both operands have the same size: We can use _Static_assert (since C11) to make sure of that. Unfortunately, _Static_assert is a declaration, not an expression, so we have to wrap it up:

#define static_assert_expr(...) \
    ((void) (struct{_Static_assert(__VA_ARGS__); int _;}) {0})

We are creating a compound literal that contains the assertion and discarding the expression.

We can easily integrate this in the previous two implementations using the comma operator:

#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) \
)

Known and Unfixable Issues

Because of how macros work, we can not use this if the punned type contains a comma:

bit_cast(int[0,1], x)

This doesn't work because macros ignore square brackets and the 1] would not be considered part of the type, but would go into __VA_ARGS__.