c++ - 如何通过不同类型重新解释数据? (类型双关困惑)

标签 c++ strict-aliasing type-punning

#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 行继续不引发警告,即使指向相同内存位置的指针现在保留在这里。这似乎是一个 bug在 gcc 中。

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

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

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

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

关于c++ - 如何通过不同类型重新解释数据? (类型双关困惑),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36413340/

相关文章:

c++ - boost ptime 线程安全与否?

c++ - NSMutableArray 到 std::vector

c++ - boost::bind 打破了严格的别名规则?

c - 如何将 dlsym() 的返回值分配给函数类型?

c - 与严格别名相关的有效类型规则

c++ - 使用模板输入双关语

c++ - 使用 Opengl 在 Window 上显示菜单

c - printf 是如何工作的?为什么输出不同?

c - 在 C 中通过 union 指针进行类型双关是否合法?

C++ MPI:数组上的 std::merge