如何通过不同类型重新解释数据? (类型双关混乱)

How do I reinterpret data through a different type? (type punning confusion)

#include <iostream>

int main(int argc, char * argv[])
{
    int a = 0x3f800000;

    std::cout << a << std::endl;

    static_assert(sizeof(float) == sizeof(int), "Oops");

    float f2 = *reinterpret_cast<float *>(&a);

    std::cout << f2 << std::endl;

    void * p = &a;
    float * pf = static_cast<float *>(p);
    float f3 = *pf;

    std::cout << f3 << std::endl;

    float f4 = *static_cast<float *>(static_cast<void *>(&a));

    std::cout << f4 << std::endl;
}

我从我可靠的编译器中得到以下信息:

me@Mint-VM ~/projects $ g++-5.3.0 -std=c++11 -o pun pun.cpp -fstrict-aliasing -Wall
pun.cpp: In function ‘int main(int, char**)’:
pun.cpp:11:45: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     float f2 = *reinterpret_cast<float *>(&a);
                                             ^
pun.cpp:21:61: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     float f4 = *static_cast<float *>(static_cast<void *>(&a));
                                                             ^
me@Mint-VM ~/projects $ ./pun
1065353216
1
1
1
me@Mint-VM ~/projects $ g++-5.3.0 --version
g++-5.3.0 (GCC) 5.3.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

我真的不明白什么时候以及为什么我会在某些地方出现双关错误,而在其他地方却没有。

所以,strict aliasing:

Strict aliasing is an assumption, made by the C (or C++) compiler, that dereferencing pointers to objects of different types will never refer to the same memory location (i.e. alias each other.)

第 11 行声称我正在打破严格别名。我没有看到这可能会伤害任何东西的情况——指针“开始存在”,立即被取消引用,然后被丢弃。很可能,这将编译为零指令。这看起来绝对没有风险 - 我告诉编译器我想要什么。

第 15-16 行继续不引发警告,即使指向相同内存位置的指针现在保留在这里。这似乎是 gcc 中的 bug

第 21 行引发警告,表明这不仅限于 reinterpret_cast。

Unions are no better(强调我的):

...it's undefined behavior to read from the member of the union that wasn't most recently written. Many compilers implement, as a non-standard language extension, the ability to read inactive members of a union.

This link 谈论使用 memcpy,但这似乎只是隐藏了您真正想要完成的事情。

对于某些系统,将指针写入 int 寄存器或接收传入字节流并将这些字节 assemble 转换为浮点数或其他非整数类型是必需的操作。

执行此操作的正确、符合标准的方法是什么?

使用memcpy:

memcpy(&f2, &a, sizeof(float));

如果你担心类型安全和语义,你可以轻松编写一个包装器:

void convert(float& x, int a) {
    memcpy(&x, &a, sizeof(float));
}

如果你愿意,你可以制作这个包装模板来满足你的需要。

请在此感谢 Anton。他的回答是第一个而且他是正确的。

我发布这个说明是因为我知道你在看到汇编程序之前不会相信他:

鉴于:

#include <cstring>
#include <iostream>

// prevent the optimiser from eliding this function altogether
__attribute__((noinline))
float convert(int in)
{
    static_assert(sizeof(float) == sizeof(int), "Oops");
    float result;
    memcpy(&result, &in, sizeof(result));
    return result;
}

int main(int argc, char * argv[])
{
    int a = 0x3f800000;
    float f = convert(a);


    std::cout << a << std::endl;
    std::cout << f << std::endl;
}

结果:

1065353216
1

使用 -O2 编译,这里是函数 convert 的汇编器输出,为了清楚起见添加了一些注释:

#
# I'll give you £10 for every call to `memcpy` you can find...
#
__Z7converti:                           ## @_Z7converti
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
#
# here's the conversion - simply move the integer argument (edi)
# into the first float return register (xmm0)
#
    movd    %edi, %xmm0
    popq    %rbp
    retq
    .cfi_endproc
#
# did you see any memcpy's? 
# nope, didn't think so.
#

为了说明问题,这里是使用 -O2 和 -fomit-frame-pointer 编译的相同函数:

__Z7converti:                           ## @_Z7converti
    .cfi_startproc
## BB#0:
    movd    %edi, %xmm0
    retq
    .cfi_endproc

记住,这个函数之所以存在,是因为我添加了防止编译器内联它的属性。实际上,启用优化后,整个功能将被优化掉。函数中的那 3 行代码和调用站点的调用将消失。

现代优化编译器很棒。

but what I really wanted was this std::cout << *reinterpret_cast<float *>(&a) << std::endl; and I think it expresses my intent perfectly well.

嗯,是的。但是 c++ 在设计时考虑了正确性和性能。很多时候,编译器会假设两个指针或两个引用不指向同一块内存。如果它能做到这一点,它就可以进行各种巧妙的优化(通常涉及不打扰进行不需要产生所需效果的读取或写入)。但是,因为写入一个指针 可能 影响从另一个指针读取(如果它们真的指向同一个对象),那么为了正确性,编译器可能不会假设两个对象是不同的,它必须执行您在代码中指示的每一次读取和写入 - 以防万一一次写入影响后续读取......除非指针指向不同的类型。如果它们指向不同的类型,则允许编译器假定它们永远不会指向同一内存 - 这是严格的别名规则。

当你这样做时:*reinterpret_cast<float *>(&a),

您正在尝试通过 int 指针和 float 指针读取相同的内存。因为指针的类型不同,所以编译器会假定它们指向不同的内存地址——即使在您看来它们并非如此。

这是结构别名规则。它可以帮助程序快速正确地执行。像这样的重新解释转换可以防止任何一个。

如您所知,reinterpret_cast 不能用于类型双关

  • Is reinterpret_cast type punning actually undefined behavior?
  • Is reinterpret_cast mostly useless?

从 C++20 开始,您将有一种通过 std::bit_cast

进行类型双关的安全方法
uint32_t bit_pattern = 0x3f800000U;
constexpr auto f = std::bit_cast<float>(bit_pattern);

目前std::bit_cast is only supported by MSVC

在等待其他人实现它的同时,如果您使用的是 Clang,则可以尝试 __builtin_bit_cast。就这样投吧

float f = __builtin_bit_cast(float, bit_pattern);

demo on Godbolt


在其他编译器或旧的 C++ 标准中,唯一的方法是通过 memcpy

然而,许多编译器有特定于实现的方法来进行类型双关,或关于类型双关的特定于实现的行为。例如在 GCC 中你可以使用 __attribute__((__may_alias__))

union Float
{
    float __attribute__((__may_alias__)) f;
    uint32_t __attribute__((__may_alias__)) u;
};

uint32_t getFloatBits(float v)
{
    Float F;
    F.f = v;
    return F.u;
}

ICC 和 Clang 也支持该属性。参见 demo