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*)(&x) = 1.5;
更正确。将对象解释为一系列字节的正确方法是将其视为char[]
。
【参考方案1】:
它未定义的原因是因为无法保证int
和float
的值表示究竟是什么。 C++ 标准没有说 float
存储为 IEEE 754 单精度浮点数。关于您将具有值0xffff
的int
对象视为float
,该标准究竟应该说什么?除了它是未定义的事实之外,它什么也没说。
然而,实际上,这是reinterpret_cast
的目的——告诉编译器忽略它所知道的关于对象类型的所有内容,并相信你这个int
实际上是一个float
。它几乎总是用于特定于机器的位级跳棋。一旦你这样做了,C++ 标准就不能保证任何事情。到那时,您需要准确了解您的编译器和机器在这种情况下的作用。
union
和 reinterpret_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的主要内容,如果未能解决你的问题,请参考以下文章
将 JS 数字数组传递给 emscripten C++ 而无需 reinterpret_cast