Implementing std::bit_cast equivalent in C

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__.