在 C++ 中进行类型双关的现代、正确的方法是什么?
What is the modern, correct way to do type punning in C++?
好像有两种C++。实用的C++和语言律师C++。在某些情况下,能够将一种类型的位模式解释为另一种类型可能很有用。浮点技巧是一个著名的例子。让我们采用著名的快速平方根反比(取自Wikipedia, which was in turn taken from here):
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
撇开细节不谈,它使用了 IEEE-754 浮点位表示的某些属性。这里有趣的部分是从 float*
到 long*
的 *(long*)
转换。 C 和 C++ 之间关于哪些类型的此类重新解释转换是定义的行为存在差异,但在实践中,两种语言都经常使用此类技术。
事实是,对于这样一个简单的问题,上面介绍的方法和其他方法可能会出现很多陷阱。举几个例子:
- strict aliasing
- lifetime issues
- 字节顺序
- 对齐
同时,进行类型双关的方法有很多,与之相关的机制也有很多。这些是我能找到的所有内容:
reinterpret_cast
和 c 风格转换
[[nodiscard]] float int_to_float1(int x) noexcept
{
return *reinterpret_cast<float*>(&x);
}
[[nodiscard]] float int_to_float2(int x) noexcept
{
return *(float*)(&x);
}
static_cast
和 void*
[[nodiscard]] float int_to_float3(int x) noexcept
{
return *static_cast<float*>(static_cast<void*>(&x));
}
std::bit_cast
[[nodiscard]] constexpr float int_to_float4(int x) noexcept
{
return std::bit_cast<float>(x);
}
memcpy
[[nodiscard]] float int_to_float5(int x) noexcept
{
float destination;
memcpy(&destination, &x, sizeof(x));
return destination;
}
union
[[nodiscard]] float int_to_float6(int x) noexcept
{
union {
int as_int;
float as_float;
} destination{x};
return destination.as_float;
}
放置 new
和 std::launder
[[nodiscard]] float int_to_float7(int x) noexcept
{
new(&x) float;
return *std::launder(reinterpret_cast<float*>(&x));
}
std::byte
[[nodiscard]] float int_to_float8(int x) noexcept
{
return *reinterpret_cast<float*>(reinterpret_cast<std::byte*>(&x));
}
问题是这些方式中哪些是安全的,哪些是不安全的,哪些是永远该死的。应该使用哪一个,为什么?是否有 C++ 社区接受的规范?为什么新版本的 C++ 在 C++17 中引入更多机制 std::launder
或在 C++20 中引入 std::byte
、std::bit_cast
?
给出一个具体的问题:重写快速平方根反函数的最安全、最高效、最好的方法是什么?(是的,我知道有维基百科上的一种方式的建议)。
编辑: 雪上加霜的是,好像还有a proposal that suggests adding yet another type punning mechanism: std::start_lifetime_as
, which is also discussed in .
(godbolt)
首先,你假设sizeof(long) == sizeof(int) == sizeof(float)
。这并不总是正确的,并且完全未指定(取决于平台)。实际上,这在我的 Windows 使用 clang-cl 时是正确的,而在我的 Linux 使用相同的 64 位机器时是错误的。相同 OS/machine 上的不同编译器可能会给出不同的结果。至少需要一个静态断言来避免偷偷摸摸的错误。
由于严格的别名规则,纯 C 类型转换、重新解释类型转换和静态类型转换在这里无效(迂腐地说,在这种情况下,程序在 C++ 标准方面是错误的)。联合解决方案也无效(它仅在 C 中有效,在 C++ 中无效)。只有 std::bit_cast
和 std::memcpy
解决方案是“安全的”(假设类型的大小在目标平台上匹配)。使用 std::memcpy
通常很快,因为它已被大多数主流编译器优化(启用优化时,例如 GCC/Clang 使用 -O3
):可以内联和替换 std::memcpy
调用通过更快的指令。 std::bit_cast
是执行此操作的新方法(仅自 C++20 起)。最后一个解决方案对于 C++ 代码更清晰,因为 std::memcpy
使用不安全的 void*
类型,从而绕过类型系统。
这是我使用 -O3
:
从 gcc 11.1 得到的
int_to_float4(int):
movd xmm0, edi
ret
int_to_float1(int):
movd xmm0, edi
ret
int_to_float2(int):
movd xmm0, edi
ret
int_to_float3(int):
movd xmm0, edi
ret
int_to_float5(int):
movd xmm0, edi
ret
int_to_float6(int):
movd xmm0, edi
ret
int_to_float7(int):
mov DWORD PTR [rsp-4], edi
movss xmm0, DWORD PTR [rsp-4]
ret
int_to_float8(int):
movd xmm0, edi
ret
我不得不添加一个 auto x = &int_to_float4;
来强制 gcc 为 int_to_float4
实际发出任何东西,我想这就是它首先出现的原因。
我对 std::launder
不是很熟悉,所以我不知道为什么不同。否则它们是相同的。这就是 gcc 必须要说的(在这种情况下,带有那个标志)。标准所说的是不同的故事。虽然 memcpy(&destination, &x, sizeof(x));
定义明确,大多数编译器都知道如何优化它。 std::bit_cast
是在 C++20 中引入的,目的是使此类转换更加明确。请注意,在 cppreference 的可能实现中,他们使用 std::memcpy
;).
TL;DR
what would be the safest, most performant and best way to rewrite the fast inverse square root function?
std::memcpy
和 C++20 及更高版本 std::bit_cast
.
好像有两种C++。实用的C++和语言律师C++。在某些情况下,能够将一种类型的位模式解释为另一种类型可能很有用。浮点技巧是一个著名的例子。让我们采用著名的快速平方根反比(取自Wikipedia, which was in turn taken from here):
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
撇开细节不谈,它使用了 IEEE-754 浮点位表示的某些属性。这里有趣的部分是从 float*
到 long*
的 *(long*)
转换。 C 和 C++ 之间关于哪些类型的此类重新解释转换是定义的行为存在差异,但在实践中,两种语言都经常使用此类技术。
事实是,对于这样一个简单的问题,上面介绍的方法和其他方法可能会出现很多陷阱。举几个例子:
- strict aliasing
- lifetime issues
- 字节顺序
- 对齐
同时,进行类型双关的方法有很多,与之相关的机制也有很多。这些是我能找到的所有内容:
reinterpret_cast
和 c 风格转换[[nodiscard]] float int_to_float1(int x) noexcept { return *reinterpret_cast<float*>(&x); } [[nodiscard]] float int_to_float2(int x) noexcept { return *(float*)(&x); }
static_cast
和void*
[[nodiscard]] float int_to_float3(int x) noexcept { return *static_cast<float*>(static_cast<void*>(&x)); }
std::bit_cast
[[nodiscard]] constexpr float int_to_float4(int x) noexcept { return std::bit_cast<float>(x); }
memcpy
[[nodiscard]] float int_to_float5(int x) noexcept { float destination; memcpy(&destination, &x, sizeof(x)); return destination; }
union
[[nodiscard]] float int_to_float6(int x) noexcept { union { int as_int; float as_float; } destination{x}; return destination.as_float; }
放置
new
和std::launder
[[nodiscard]] float int_to_float7(int x) noexcept { new(&x) float; return *std::launder(reinterpret_cast<float*>(&x)); }
std::byte
[[nodiscard]] float int_to_float8(int x) noexcept { return *reinterpret_cast<float*>(reinterpret_cast<std::byte*>(&x)); }
问题是这些方式中哪些是安全的,哪些是不安全的,哪些是永远该死的。应该使用哪一个,为什么?是否有 C++ 社区接受的规范?为什么新版本的 C++ 在 C++17 中引入更多机制 std::launder
或在 C++20 中引入 std::byte
、std::bit_cast
?
给出一个具体的问题:重写快速平方根反函数的最安全、最高效、最好的方法是什么?(是的,我知道有维基百科上的一种方式的建议)。
编辑: 雪上加霜的是,好像还有a proposal that suggests adding yet another type punning mechanism: std::start_lifetime_as
, which is also discussed in
(godbolt)
首先,你假设sizeof(long) == sizeof(int) == sizeof(float)
。这并不总是正确的,并且完全未指定(取决于平台)。实际上,这在我的 Windows 使用 clang-cl 时是正确的,而在我的 Linux 使用相同的 64 位机器时是错误的。相同 OS/machine 上的不同编译器可能会给出不同的结果。至少需要一个静态断言来避免偷偷摸摸的错误。
由于严格的别名规则,纯 C 类型转换、重新解释类型转换和静态类型转换在这里无效(迂腐地说,在这种情况下,程序在 C++ 标准方面是错误的)。联合解决方案也无效(它仅在 C 中有效,在 C++ 中无效)。只有 std::bit_cast
和 std::memcpy
解决方案是“安全的”(假设类型的大小在目标平台上匹配)。使用 std::memcpy
通常很快,因为它已被大多数主流编译器优化(启用优化时,例如 GCC/Clang 使用 -O3
):可以内联和替换 std::memcpy
调用通过更快的指令。 std::bit_cast
是执行此操作的新方法(仅自 C++20 起)。最后一个解决方案对于 C++ 代码更清晰,因为 std::memcpy
使用不安全的 void*
类型,从而绕过类型系统。
这是我使用 -O3
:
int_to_float4(int):
movd xmm0, edi
ret
int_to_float1(int):
movd xmm0, edi
ret
int_to_float2(int):
movd xmm0, edi
ret
int_to_float3(int):
movd xmm0, edi
ret
int_to_float5(int):
movd xmm0, edi
ret
int_to_float6(int):
movd xmm0, edi
ret
int_to_float7(int):
mov DWORD PTR [rsp-4], edi
movss xmm0, DWORD PTR [rsp-4]
ret
int_to_float8(int):
movd xmm0, edi
ret
我不得不添加一个 auto x = &int_to_float4;
来强制 gcc 为 int_to_float4
实际发出任何东西,我想这就是它首先出现的原因。
我对 std::launder
不是很熟悉,所以我不知道为什么不同。否则它们是相同的。这就是 gcc 必须要说的(在这种情况下,带有那个标志)。标准所说的是不同的故事。虽然 memcpy(&destination, &x, sizeof(x));
定义明确,大多数编译器都知道如何优化它。 std::bit_cast
是在 C++20 中引入的,目的是使此类转换更加明确。请注意,在 cppreference 的可能实现中,他们使用 std::memcpy
;).
TL;DR
what would be the safest, most performant and best way to rewrite the fast inverse square root function?
std::memcpy
和 C++20 及更高版本 std::bit_cast
.