在C ++中进行类型修饰的现代正确方法是什么? [英] What is the modern, correct way to do type punning in C++?

查看:81
本文介绍了在C ++中进行类型修饰的现代正确方法是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

似乎有两种类型的C ++.实用的C ++和语言律师C ++.在某些情况下,将一种类型的位模式解释为另一种类型可能很有用.浮点技巧是一个明显的例子.让我们以著名的快速逆平方根为基础(取自 Wikipedia ,该取自 Wikipedia ).href ="https://github.com/id-Software/Quake-III-Arena/blob/master/code/game/q_math.c#L552" rel ="noreferrer">此处):

It seems like there are two types of C++. The practical C++ and the language lawyer C++. In certain situations it can be useful to be able to interpret a bit pattern of one type as if it were a different type. Floating point tricks are a notable example. Let's take the famous fast inverse square root (taken from 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 ++之间在定义行为的重定义类型方面存在差异,但是实际上在两种语言中都经常使用这种技术.

Setting aside details, it uses certain properties of the IEEE-754 floating point bit representation. The interesting part here is the *(long*) cast from float* to long*. There are differences between C and C++ about which types of such reinterpreting casts are defined behaviour, however in practice such techniques are used often in both languages.

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

The thing is that for such a simple problem there are a lot of pitfalls that can occur with the approach presented above and different others. To name some:

同时,有许多执行类型修剪的方法以及与之相关的许多机制.这些都是我能找到的:

At the same time, there are a lot of ways of performing type punning and a lot of mechanisms related to it. These are all that I could find:

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

  • 联盟

    [[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 std :: byte std :: bit_cast 在C ++ 20中?

    The question is which of these ways are safe, which are unsafe and which are damned forever. Which one should be used and why? Is there a canonical one accepted by the C++ community? Why are new versions of C++ introducing even more mechanisms std::launder in C++17 or std::byte, std::bit_cast in C++20?

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

    To give a concrete problem: what would be the safest, most performant and best way to rewrite the fast inverse square root function? (Yes, I know that there is a suggestion of one way on Wikipedia).

    ( godbolt )

    推荐答案

    首先,您假设 sizeof(long)== sizeof(int)== sizeof(float).这并不总是正确的,并且完全没有指定(取决于平台).实际上,这在使用clang-cl的Windows上是正确的,而在使用同一64位计算机的Linux上则是错误的.同一台OS/计算机上的不同编译器可以给出不同的结果.至少需要一个静态断言才能避免偷偷摸摸的错误.

    First of all, you assume that sizeof(long) == sizeof(int) == sizeof(float). This is not always true, and totally unspecified (platform dependent). Actually, this is true on my Windows using clang-cl and wrong on my Linux using the same 64-bit machine. Different compilers on the same OS/machine can give different results. A static assert is at least required to avoid sneaky bugs.

    纯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 * 类型,从而绕过类型系统.

    Plain C casts, reinterpret casts and static casts are invalid here because of the strict aliasing rule (to be pedantic, the program is ill-formed in this case regarding the C++ standard). The union solution is not valid too (it is only valid in C, not in C++). Only the std::bit_cast and the std::memcpy solution are "safe" (assuming the size of the types are matching on the target plateform). Using std::memcpy is often fast as it is optimized by most mainstream compiler (when optimizations are enabled, like with -O3 for GCC/Clang): the std::memcpy call can be inlined and replaced by faster instructions. std::bit_cast is the new way of doing this (only since C++20). The last solution is cleaner for a C++ code as std::memcpy use unsafe void* types and thus by-pass the type system.

    这篇关于在C ++中进行类型修饰的现代正确方法是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆