在 C 中实现 std::bit_cast 等价物
Implementing std::bit_cast equivalent in C
是否可以在 C 中实现类似于 C++20 的 std::bit_cast
的东西?这比使用 union
或将指针转换为不同类型并取消引用要方便得多。
如果你有 bit_cast
,那么实现一些浮点函数会更容易:
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__))
但它有主要问题:
- 这是未定义的行为,因为它违反了严格的别名
- 它不适用于位转换右值,因为我们直接获取第二个操作数的地址
- 它不能确保操作数具有相同的大小
我们可以实现没有这些问题的 bit_cast
吗?
在非标准标准 C 中是可能的,感谢 typeof
。 typeof
is also a further proposed feature for C23,所以在标准C23中可能成为可能。下面的解决方案之一做出了一些牺牲,以允许符合 C99。
使用union
实现
让我们先看看使用 union
的方法是如何工作的:
#define bit_cast(T, ...) \
((union{typeof(T) a; typeof(__VA_ARGS__) b;}) {.b=(__VA_ARGS__)}.a)
我们正在从由 T
和给定表达式具有的任何类型组成的匿名联合创建复合文字。我们使用指定的初始化器将此文字初始化为 .b= ...
,然后访问类型为 T
.
的 .a
成员
由于 C 的类型语法,如果我们想双关函数指针、数组等,typeof(T)
是必需的。
使用memcpy
实现
这个实现稍长一些,但具有仅依赖 C99 的优点,甚至可以在不使用 typeof
:
的情况下工作
#define bit_cast(T, ...) \
(*(typeof(T)*) memcpy(&(T){0}, &(typeof(__VA_ARGS__)) {(__VA_ARGS__)}, sizeof(T)))
我们正在从一个复合文字复制到另一个复合文字,然后访问目标值:
- 源文字是我们输入表达式的一个副本,它允许我们获取它的地址,即使对于
bit_cast(float, 123)
,其中 123
是一个右值
- 目标是
T
类型的零初始化文字
memcpy
returns 目标操作数,因此我们可以将结果转换为 typeof(T)*
然后取消引用该指针。
我们可以在这里完全消除typeof
并使这个C99兼容,但也有缺点:
#define bit_cast(T, ...) \
(*((T*) memcpy(&(T){0}, &(__VA_ARGS__), sizeof(T))))
我们现在直接获取表达式的地址,所以我们不能再对右值使用 bit_cast
。我们正在使用 T*
而没有 typeof
,所以我们不能再转换为函数指针、数组等
实施大小检查(自 C11 起)
至于最后一个问题,即我们不验证两个操作数是否具有相同的大小:我们可以使用 _Static_assert
(自 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__
.
是否可以在 C 中实现类似于 C++20 的 std::bit_cast
的东西?这比使用 union
或将指针转换为不同类型并取消引用要方便得多。
如果你有 bit_cast
,那么实现一些浮点函数会更容易:
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__))
但它有主要问题:
- 这是未定义的行为,因为它违反了严格的别名
- 它不适用于位转换右值,因为我们直接获取第二个操作数的地址
- 它不能确保操作数具有相同的大小
我们可以实现没有这些问题的 bit_cast
吗?
在非标准标准 C 中是可能的,感谢 typeof
。 typeof
is also a further proposed feature for C23,所以在标准C23中可能成为可能。下面的解决方案之一做出了一些牺牲,以允许符合 C99。
使用union
实现
让我们先看看使用 union
的方法是如何工作的:
#define bit_cast(T, ...) \
((union{typeof(T) a; typeof(__VA_ARGS__) b;}) {.b=(__VA_ARGS__)}.a)
我们正在从由 T
和给定表达式具有的任何类型组成的匿名联合创建复合文字。我们使用指定的初始化器将此文字初始化为 .b= ...
,然后访问类型为 T
.
.a
成员
由于 C 的类型语法,如果我们想双关函数指针、数组等,typeof(T)
是必需的。
使用memcpy
实现
这个实现稍长一些,但具有仅依赖 C99 的优点,甚至可以在不使用 typeof
:
#define bit_cast(T, ...) \
(*(typeof(T)*) memcpy(&(T){0}, &(typeof(__VA_ARGS__)) {(__VA_ARGS__)}, sizeof(T)))
我们正在从一个复合文字复制到另一个复合文字,然后访问目标值:
- 源文字是我们输入表达式的一个副本,它允许我们获取它的地址,即使对于
bit_cast(float, 123)
,其中123
是一个右值 - 目标是
T
类型的零初始化文字
memcpy
returns 目标操作数,因此我们可以将结果转换为 typeof(T)*
然后取消引用该指针。
我们可以在这里完全消除typeof
并使这个C99兼容,但也有缺点:
#define bit_cast(T, ...) \
(*((T*) memcpy(&(T){0}, &(__VA_ARGS__), sizeof(T))))
我们现在直接获取表达式的地址,所以我们不能再对右值使用 bit_cast
。我们正在使用 T*
而没有 typeof
,所以我们不能再转换为函数指针、数组等
实施大小检查(自 C11 起)
至于最后一个问题,即我们不验证两个操作数是否具有相同的大小:我们可以使用 _Static_assert
(自 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__
.