C++ 联合与 reinterpret_cast

Posted

技术标签:

【中文标题】C++ 联合与 reinterpret_cast【英文标题】:C++ unions vs. reinterpret_cast 【发布时间】:2013-05-19 16:52:37 【问题描述】:

从other *** questions 和阅读ISO/IEC draft C++ standard 标准的§9.5.1 看来,使用联合来执行数据的文字reinterpret_cast 是未定义的行为。

考虑下面的代码。目标是获取0xffff 的整数值并将其解释为IEEE 754 浮点中的一系列位。 (Binary convert shows visually how this is done.)

#include <iostream>
using namespace std;

union unionType 
    int myInt;
    float myFloat;
;

int main() 

    int i = 0xffff;

    unionType u;
    u.myInt = i;

    cout << "size of int    " << sizeof(int) << endl;
    cout << "size of float  " << sizeof(float) << endl;

    cout << "myInt          " << u.myInt << endl;
    cout << "myFloat        " << u.myFloat << endl;

    float theFloat = *reinterpret_cast<float*>(&i);
    cout << "theFloat       " << theFloat << endl;

    return 0;

此代码的输出是预期的,同时使用 GCC 和 clang 编译器。

size of int    4
size of float  4
myInt          65535
myFloat        9.18341e-41
theFloat       9.18341e-41

我的问题是,标准是否实际上排除了myFloat 的值是确定性的?是否以任何方式使用reinterpret_cast更好来执行这种类型的转换?

该标准在 §9.5.1 中规定了以下内容:

在一个联合中,在任何时候最多可以有一个非静态数据成员处于活动状态,即最多一个非静态数据成员的值可以存储在一个联合中 [...] 联合体的大小足以包含其最大的非静态数据成员。每个非静态数据成员都被分配,就好像它是结构的唯一成员一样。 联合对象的所有非静态数据成员具有相同的地址。

最后一句,保证所有非静态成员具有相同的地址,似乎表明联合的使用保证reinterpret_cast的使用相同,但是先前关于活动数据成员的声明似乎排除了这种保证。

那么哪个结构更正确?

编辑: 使用 Intel 的 icpc 编译器,上面的代码会产生更有趣的结果:

$ icpc union.cpp
$ ./a.out
size of int    4
size of float  4
myInt          65535
myFloat        0
theFloat       0

【问题讨论】:

这是 UB。这不比说uint32_t x; *(float*)(&amp;x) = 1.5; 更正确。将对象解释为一系列字节的正确方法是将其视为char[] 【参考方案1】:

它未定义的原因是因为无法保证intfloat 的值表示究竟是什么。 C++ 标准没有说 float 存储为 IEEE 754 单精度浮点数。关于您将具有值0xffffint 对象视为float,该标准究竟应该说什么?除了它是未定义的事实之外,它什么也没说。

然而,实际上,这是reinterpret_cast 的目的——告诉编译器忽略它所知道的关于对象类型的所有内容,并相信你这个int 实际上是一个float。它几乎总是用于特定于机器的位级跳棋。一旦你这样做了,C++ 标准就不能保证任何事情。到那时,您需要准确了解您的编译器和机器在这种情况下的作用。

unionreinterpret_cast 方法都是如此。我建议reinterpret_cast 对这项任务“更好”,因为它使意图更清晰。但是,保持代码定义明确始终是最好的方法。

【讨论】:

这是否意味着在这里使用未定义的行为是“有效的”,尽管编译器也可以说“好吧,这是未定义的行为,我不需要让这个工作”? @MatsPetersson 这取决于您所说的“有效”是什么意思。如果您想获得 C++ 标准的支持,知道使用符合标准的编译器,您的程序对于某些特定输入有一个明确定义的执行路径,那么您绝对不想调用未定义的行为。但是,如果您可以保证具有未定义行为的程序在任何情况下都能正常工作,那么您可能会认为它是“有效的”——它只是不像 C++ 那样“定义明确”。任何未定义的行为都是如此。 @MatsPetersson:相当多的“未定义”行为实际上是实现定义的,因为实现者已经选择了如何编译代码。因此,您不必依赖不应该工作的东西,而只是依赖编译器和平台之间可能不同的东西。 @JonPurdy 没错。未定义的行为只是意味着实现不必记录它。在reinterpret_cast 的情况下,无论它是否未定义,它都没有做任何特别的事情。它告诉编译器 not 检查强制转换是否安全。无论哪种方式,你都会得到一个编译器认为是新类型的指针。 @sftrabbit 好的,感谢您澄清这一点。似乎很多时候,人们很快就会指出某些东西是 UB,好像要不惜一切代价避免它,这就是我问这个问题的原因。当然,如果它是定义的实现,我想存在同一个编译器的另一个版本可能表现不同的风险,这可能会导致工作中出现问题(这也是大型项目非常不愿意更改编译器的一个原因,即使新的编译器“好得多”[根据我们可能有的更好的定义])。【参考方案2】:

这不是未定义的行为。它是实现定义的行为。第一个确实意味着可能会发生坏事。另一个意味着将要发生的事情必须由实现来定义。

reinterpret_cast 违反了严格的别名规则。所以我认为它不会可靠地工作。联合技巧就是人们所说的type-punning,编译器通常允许这样做。 gcc 人员记录了编译器的行为:http://gcc.gnu.org/onlinedocs/gcc/Structures-unions-enumerations-and-bit_002dfields-implementation.html#Structures-unions-enumerations-and-bit_002dfields-implementation

我认为这也应该适用于 icpc(但他们似乎没有记录他们是如何实现的)。但是当我查看程序集时,看起来 icc 试图用浮点数作弊并使用更高精度的浮点数。将-fp-model source 传递给编译器修复了该问题。使用该选项,我得到与 gcc 相同的结果。 我认为您一般不会使用此标志,这只是验证我的理论的测试。

所以对于 icpc,我认为如果您将代码从 int/float 切换到 long/double,类型双关也将适用于 icpc。

【讨论】:

有趣的是,除非设置了 -fp-model source,否则切换到 long/double 会产生与 int/float 转换相同的输出。 您正在编译 64 位二进制文​​件吗?我已经在最新的 iclc 上用 -O3 尝试了 long/double 的问题,我在联合中得到了预期的结果 是的,在 64 位 OSX 上使用“icpc (ICC) 13.0.2 20130314”,创建一个 64 位二进制文​​件。使用 -O3 和不使用 -O3 的输出没有区别。 嗯,Linux 上的 wfm。但由于它没有记录在案,我并不感到惊讶。然而,我很惊讶英特尔没有记录某种形式的双关语(或者我只是不擅长搜索他们的文档)【参考方案3】:

未定义的行为并不意味着坏事一定会发生。这意味着语言定义不会告诉您会发生什么。这种类型的双关语自远古时代(即自 1969 年以来)就已成为 C 和 C++ 编程的一部分;编写一个不起作用的编译器需要一个特别反常的实现者。

【讨论】:

以上是关于C++ 联合与 reinterpret_cast的主要内容,如果未能解决你的问题,请参考以下文章

C++标准转换运算符reinterpret_cast

强强联合 | 易云股份与帝联科技达成合作,深耕行业云市场

将 JS 数字数组传递给 emscripten C++ 而无需 reinterpret_cast

C++:标准转换运算符reinterpret_cast

reinterpret_cast and const_cast

c++知识点总结--静态与动态联编