在 C++ 中进行类型双关的现代正确方法是什么?

jan*_*b04 35 c++ performance casting language-lawyer type-punning

似乎有两种类型的 C++。实用C++和语言律师C++。在某些情况下,能够将一种类型的位模式解释为一种不同的类型会很有用。浮点技巧就是一个显着的例子。让我们取著名的快速平方根反比(取自Wikipedia,又取自此处):

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;
}
Run Code Online (Sandbox Code Playgroud)

撇开细节不谈,它使用 IEEE-754 浮点位表示的某些属性。这里有趣的部分是*(long*)float*to的演员表long*。C 和 C++ 之间在哪些类型的这种重新解释强制转换是定义的行为方面存在差异,但实际上这种技术在两种语言中都经常使用。

问题是,对于这样一个简单的问题,上面介绍的方法和其他不同的方法可能会出现很多陷阱。列举一些:

同时,执行类型双关的方法有很多,相关的机制也很多。我能找到的只有这些:

问题是这些方式中哪些是安全的,哪些是不安全的,哪些是永远被诅咒的。应该使用哪一个,为什么?是否有 C++ 社区接受的规范?为什么++的C的新版本引入更加机制std::launder在C ++ 17或者std::bytestd::bit_cast在C ++ 20?

给出一个具体问题:重写快速平方根反函数的最安全、最高效和最好的方法是什么?(是的,我知道维基百科上有一种方法的建议)。

编辑:为了增加混乱,似乎有一个建议建议添加另一种类型的双关机制:std::start_lifetime_as,这也在另一个问题中讨论过。

天马行空

Jér*_*ard 15

首先,你假设sizeof(long) == sizeof(int) == sizeof(float). 这并不总是正确的,并且完全未指定(取决于平台)。实际上,这在我使用 clang-cl 的 Windows 上是正确的,在使用相同 64 位机器的 Linux 上是错误的。同一操作系统/机器上的不同编译器会给出不同的结果。至少需要静态断言以避免偷偷摸摸的错误。

由于严格的别名规则,简单的 C 类型转换、重新解释类型转换和静态类型转换在这里是无效的(迂腐,在这种情况下,程序在 C++ 标准方面格式错误)。联合解决方案也无效(它仅在 C 中有效,在 C++ 中无效)。只有std::bit_caststd::memcpy解决方案是“安全的”(假设类型的大小在目标平台上匹配)。使用std::memcpy通常很快,因为它已被大多数主流编译器优化(启用优化时,例如-O3GCC/Clang):std::memcpy调用可以被内联并由更快的指令替换。std::bit_cast是这样做的新方法(仅自 C++20 起)。最后一个解决方案对于 C++ 代码来说是更清晰的,因为它std::memcpy使用了不安全的void*类型,从而绕过了类型系统。


for*_*818 10

这是我从 gcc 11.1 得到的-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
Run Code Online (Sandbox Code Playgroud)

我不得不添加 aauto 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; 博士

重写快速平方根反函数的最安全、最高效和最好的方法是什么?

std::memcpy在 C++20 及更高版本中std::bit_cast

  • GCC 不会为“int_to_float4”生成任何代码,因为该函数不执行任何操作,并且您将其标记为“constexpr”。它总是会内联它,并且不会使用它,因此不需要为该函数发出代码。作为获取函数地址的替代方法,您可以删除“constexpr”。(请注意,“constexpr”函数会忽略“[[noinline]]”属性。)话虽这么说,我不明白一个特定编译器生成的程序集列表如何接近这个问题的答案。 (4认同)
  • 事实上,查看编译器输出中的琐碎示例通常会产生误导。其中几种方法单独看起来可能很好,但当放入更复杂的代码中时,可能会由于严格的别名违规等问题而严重破坏。 (2认同)