一个适当定义的联合和reinterpret_cast之间有什么区别?

11

你能提出至少一个场景,在这个场景中,两种技术之间有显著的差异吗?

union {
T var_1;
U var_2;
}

并且

var_2 = reinterpret_cast<U> (var_1)

我越想越觉得它们从实际角度看起来是一样的。

我发现的一个区别是,虽然联合体的大小与最大数据类型相同,但在这篇文章中描述的reinterpret_cast可能会导致截断,因此普通C风格的联合体甚至比新的C++转换更安全。

你能概述一下这两者之间的差异吗?


2
据我所知,在C语言中使用联合体进行类型转换是安全的 - 我不确定在C++中是否也是如此,如果不是,则必须使用类型转换。 - user529758
5
@H2CO3 我不知道(而且老实说我也不在意)使用联合体是否安全,但reinterpret_cast绝对不是更安全的选择。 - R. Martinho Fernandes
4
因为1) 到底谁需要这个东西;2) memcpy 完美地解决了这个问题,不需要选择含糊其辞的标准来使其正常工作。 - R. Martinho Fernandes
3
回答你的问题,“到底谁需要这个”,我需要。 - John Dibling
3
"sizeof" 按字节计算是因为所有对象的大小都是字节的倍数。就这样。现在可以请你停止这无意义的争论。 - Mike Seymour
显示剩余24条评论
4个回答

8
与其他答案所述相反,从实际角度来看,存在巨大的区别,尽管在标准上可能没有这样的区别。
从标准的角度来看,reinterpret_cast仅保证对往返转换有效,并且仅在中间指针类型的对齐要求不强于源类型时才有效。您不允许通过一个指针读取并从另一个指针类型读取(*)
同时,标准要求联合具有类似的行为,从联合成员中读取活动成员(最后写入的成员)以外的成员是未定义的行为(+)
然而,编译器通常为联合情况提供额外的保证,我所知道的所有编译器(VS、g++、clang++、xlC_r、intel、Solaris CC)都保证您可以通过非活动成员读出联合,并且它将生成与通过活动成员写入的完全相同的位设置值。
这在高度优化时从网络中读取特别重要:
double ntohdouble(const char *buffer) {          // [1]
   union {
      int64_t   i;
      double    f;
   } data;
   memcpy(&data.i, buffer, sizeof(int64_t));
   data.i = ntohll(data.i);
   return data.f;
}
double ntohdouble(const char *buffer) {          // [2]
   int64_t data;
   double  dbl;
   memcpy(&data, buffer, sizeof(int64_t));
   data = ntohll(data);
   dbl = *reinterpret_cast<double*>(&data);
   return dbl;
}

我所知道的所有编译器(gcc、clang、VS、sun、ibm、hp)都认可[1]中的实现,而[2]中的实现则不被认可,并且在使用强制优化时会在某些编译器中出现严重错误。特别地,我见过gcc重新排列指令并在评估ntohl之前读取dbl变量,从而产生错误结果。


(*) 有一点例外,即使原始指针类型是什么,你总是可以从[signed|unsigned] char*中读取内容。

(+) 再次提醒,如果活动成员与另一个成员共享公共前缀,则可以通过compatible成员读取该前缀。


有趣。reinterpret_cast 实现中的失败让我感到惊讶。这可以被认为是一个错误吗?还是标准对于在这种应用程序中使用转换太严格了?我不熟悉 C++ 中的网络编程。 - Marc Claesen
2
@MarcClaesen:谷歌搜索“strict aliasing”和一些粗略的词语,你会发现有人在GCC中抱怨这种行为。标准提供了一组有效别名(多个指针引用同一对象)的情况,在有限的集合之外,其他所有情况都是未定义的行为,上面的reinterpret_cast的情况就是未定义的行为。欲了解更多信息,请谷歌搜索“strict aliasing”,“no-strict-aliasing”或类似内容。 - David Rodríguez - dribeas
关于编译器如何 实际 处理这一点,你提出了很好的观点。在这种情况下,行为是由实现定义的(而且真的,如果没有浪费周期来创建随机 UB,否则一个明智的编译器怎么会定义这个行为呢?),并且说实话,在相关情况下非常有用。 - underscore_d
我想看一个reinterpret_cast失效的示例(直接URL)。这两个函数生成相同的汇编输出。 - Maxim Egorushkin
@MaximEgorushkin: 请查看gcc manpage的-fstrict-alias说明(例如此处 - David Rodríguez - dribeas
显示剩余3条评论

5

一个合适的 union 和一个(假设)合适且安全的 reinterpret_cast 之间存在一些技术上的区别,但我想不出这些区别中有哪些是无法克服的。

我认为,相对于技术原因,更喜欢使用 union 的“真正”原因在于文档说明。

假设您正在设计一堆类来表示一种线路协议(我猜这是使用类型转换的最常见原因),并且该线路协议由许多消息、子消息和字段组成。如果其中一些字段是共同的,例如消息类型、序列号等,则使用 union 简化了将这些元素绑定在一起,并有助于准确记录协议在线路上的出现方式。

使用 reinterpret_cast 同样可以实现此目的,但为了真正了解发生了什么,您必须检查从一个数据包到下一个数据包的代码。使用 union,您只需查看标头即可了解发生了什么。


@R.MartinhoFernandes:在我能想到一个好的答案之前,我会从我的回答中删除那一部分。 - John Dibling
说得好。就我所知,你把它搞反了:联合体中的对齐是由编译器保证的,而将其重新解释为具有更严格对齐的类型则是危险的。 - R. Martinho Fernandes
@R.MartinhoFernandes:不,我知道。 - John Dibling
由于目前没有出现“相关”的差异,我将接受这个答案,以防止这个问题变得非常OT或朝形而上学的方向发展。 - user2485710
除了文档目的之外,在标准保证之外,从实际角度考虑,编译器在union的情况下提供额外的保证。 - David Rodríguez - dribeas
显示剩余3条评论

1
在C++11中,联合体是一种类类型,您可以持有具有非平凡成员函数的成员。您无法简单地从一个成员转换到另一个成员。
§ 9.5.3
[示例:考虑以下联合体:]
union U {
int i;
float f;
std::string s;
};

由于 std::string(21.3)声明了所有特殊成员函数的非平凡版本,因此 U 将具有隐式删除的默认构造函数、复制/移动构造函数、复制/移动赋值运算符和析构函数。要使用 U,必须提供一些或全部这些成员函数。- 示例结束]


好的,如果我决定T和U只是POD类型呢?在这种情况下,考虑到你的回答,你基本上是在说它们是相同的,对吗? - user2485710
你可以补充说明,联合体确实是包含不同类型对象的类,但这些对象在不同的时间具有不同的类型,这与重新解释类型是根本不同的事情。 - cli_hlt
无论它们是POD类型还是非POD类型,对于C++来说都没有关系;就C++而言,这要么是实现定义的行为,要么是未定义的行为。只有在访问存储在联合体中的变量时,联合体才对C++有意义。C++没有标准受保护的机制来进行原地类型转换。 - Nicol Bolas

-1

从实际角度来看,在真实的、非虚构的计算机上,它们很可能是100%相同的。你可以将一个类型的二进制表示形式塞入另一个类型中。

从语言法律专家的角度来看,对于某些情况(例如指针到整数的转换),使用reinterpret_cast是明确定义的,否则就是特定于实现的。

另一方面,联合类型游戏非常明显是未定义的行为,总是如此(尽管未定义并不一定意味着“不起作用”)。标准规定,最多只能在联合中存储一个非静态数据成员的值。这意味着如果你设置了var1,那么var1是有效的,但var2不是。
然而,由于var1var2存储在同一内存位置,当然你仍然可以随意读写任何类型,并且假设它们具有相同的存储大小,没有任何位被“丢失”。


这不仅仅是关于计算机,还涉及到编译器。 - R. Martinho Fernandes
你最后的两段完全相互矛盾。最后一段是高效编译器实现的实际结果,而不是标准定义的任何内容。由于读取除最近写入的成员之外的成员是未定义行为,一些理论上可怕的编译器会返回完全随机的垃圾数据,而不是更明智(也更实际)的重新解释位模式的替代方案。说你可以在实践中这样做是没问题的,但这在语言层面上并不保证,因此需要明确限定。 - underscore_d
我并不是说它会破坏RAM,而是根据我的理解,如果读取“非活动”联合成员,它可能(愚蠢但可能)合法地返回无意义的乱码,这些乱码甚至不是来自RAM或者反映相同的位。是的,这种情况不太可能发生,如果有编译器真的这么愚蠢,我会无限嘲笑它。你提到UB被完全优化掉的例子更有可能发生,并且在教学上更有用(对于那些必须调试结果的可怜人来说则不然)。总之,我尽可能避免这种代码,对于其余部分,GCC实现得很直观。 - underscore_d
1
@underscore_d:你说得非常正确,编译器确实可以选择使用它仍然在寄存器中拥有的旧(但有效!)结构体成员的数字值。这不仅是合法的,而且甚至是有意义的——毕竟,由于你没有修改该成员,根据定义,该值逻辑上来说是相同的(并非事实上)。幸运的是,这种情况很少发生,需要在同一作用域内连续进行多次这样的转换。 - Damon
是的,我猜这就是标准的理由,因为它遵循与严格别名相同的(公平)逻辑。然而,有趣的是(并且在我的几个当前项目中非常相关...),如果union成员共享子成员的公共初始序列,则C++允许此操作。我猜检查所有union的繁琐性就是实际编译器允许所有联合成员进行类型转换的原因。(更进一步混淆,C委员会似乎已经变得困惑并开始考虑别名,导致了这个混乱:https://dev59.com/s1sW5IYBdhLWcg3wwZcP) - underscore_d
显示剩余2条评论

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