C++联合体 vs reinterpret_cast

22

其他 StackOverflow 问题和阅读ISO/IEC C++ 标准草案的第9.5.1节可以看出,使用联合体进行数据的字面上的reinterpret_cast是未定义的行为。

考虑下面的代码。目标是将十六进制值0xffff直接解释为IEEE 754浮点数中的一系列位。(二进制转换可视化地展示了这一过程。

#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保证相同的,但早期关于活动数据成员的声明似乎排除了此保证。
那么哪种结构更正确?
编辑: 使用英特尔的icpc编译器,上述代码产生了更有趣的结果:
$ icpc union.cpp
$ ./a.out
size of int    4
size of float  4
myInt          65535
myFloat        0
theFloat       0

2
这是未定义行为。这种做法和 uint32_t x; *(float*)(&x) = 1.5; 一样是错误的。将一个对象解释为一系列字节的正确方法是将其视为 char[] - Kerrek SB
3个回答

9
它为什么是未定义的呢?因为无法保证int和float的值的表示方式是什么。C++标准并没有说一个float被存储为IEEE 754单精度浮点数。如果你将值为0xffff的int对象视为float,标准应该怎么说呢?除了这个未定义的事实之外,它什么也没说。
然而,在实际情况下,这正是reinterpret_cast的目的——告诉编译器忽略它所知道的有关对象类型的一切,相信你这个int实际上是一个float。它几乎总是用于特定于机器的位级操作。一旦这样做,C++标准就不再保证任何东西。此时,你需要理解编译器和机器在这种情况下的确切操作。
这对于union和reinterpret_cast方法都是正确的。我建议使用reinterpret_cast更好,因为它使意图更加清晰。但是,保持代码的明确定义始终是最佳选择。

这是否意味着在此处使用未定义行为是“有效”的,尽管编译器也可以说“好吧,这是未定义的行为,我不需要使其工作”? - Mats Petersson
@MatsPetersson 这取决于您所说的“有效”是什么意思。如果您希望在符合标准的编译器支持下,确保您的程序对于某些特定输入具有一个明确定义的执行路径,那么您绝对不希望调用未定义的行为。但是,如果您可以保证具有未定义行为的程序在任何情况下都能正确工作,那么您可能认为它是“有效”的 - 它只是不符合C ++的“明确定义”。这适用于任何未定义的行为。 - Joseph Mansfield
@MatsPetersson:相当多的“未定义”行为实际上是实现定义的,因为实现者已经选择了如何编译代码。因此,您不一定依赖于不应该工作的东西,只是可能在编译器和平台之间不同的东西。 - Jon Purdy
@JonPurdy 没错。未定义行为只是意味着实现不必记录它。在 reinterpret_cast 的情况下,无论它是否未定义,它都不会执行任何特殊操作。它告诉编译器不要检查转换是否安全。无论哪种方式,您都会得到一个编译器认为是新类型的指针。 - Joseph Mansfield
@sftrabbit 好的,感谢你澄清。看起来很多时候人们会非常快地指出某些东西是UB,好像它应该被尽可能地避免,这就是我问这个问题的原因。当然,如果它是实现定义的,我想还存在这样一个风险,即同一编译器的另一个版本可能会有不同的行为,这可能会给工作带来问题(这也是为什么大型项目非常不愿意更改编译器,即使新编译器“更好”[无论我们对“更好”的定义是什么]的一个原因)。 - Mats Petersson
显示剩余2条评论

7

这不是未定义行为,而是实现定义行为。前者意味着可能会发生不良后果,而后者意味着实现必须定义将会发生什么。

reinterpret_cast 违反了严格别名规则,所以我认为它不能可靠地工作。联合体技巧是人们称之为“类型转换”,通常由编译器允许。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转换相同的输出。 - kgraney
你是否正在编译64位二进制文件?我已经在最新的iclc上使用-O3尝试了您的长/双问题,并在联合中获得了预期的结果。 - Guillaume
是的,在64位OSX上使用“icpc(ICC)13.0.2 20130314”,创建64位二进制文件。使用-O3和不使用-O3之间的输出没有区别。 - kgraney
嗯,在Linux上工作良好。但由于它没有记录,我并不感到惊讶。然而,我很惊讶英特尔没有记录某种类型转换(或者我只是在搜索他们的文档方面做得不好)。 - Guillaume

1

未定义行为并不意味着一定会发生糟糕的事情。它只是表示语言定义没有告诉你会发生什么。这种类型判断自古以来就是 C 和 C++ 编程的一部分(即自 1969 年以来);只有特别恶意的实现者才会编写一个无法正常工作的编译器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接