【问题标题】:What is the difference between a proper defined union and a reinterpret_cast?正确定义的联合和 reinterpret_cast 有什么区别?
【发布时间】:2013-07-29 05:28:21
【问题描述】:

您能否提出至少一种两者之间存在重大差异的场景

union {
T var_1;
U var_2;
}

var_2 = reinterpret_cast<U> (var_1)

?

我想得越多,它们对我来说就越像同一件事,至少从实际的角度来看是这样。

我发现的一个区别是,虽然联合大小在大小方面是最大的数据类型,但本文中描述的 reinterpret_cast 可能会导致截断,因此普通的旧 C 风格联合更加安全比更新的 C++ 转换。

你能概括一下这两者之间的区别吗?

【问题讨论】:

  • 据我所知,在 C 中使用unions 进行类型双关是安全的——我不确定 C++,也许不是,然后你必须使用类型转换。
  • @H2CO3 我不知道(老实说不关心)使用联合是否安全,但 reinterpret_cast 严格来说并不安全。
  • @user2485710 因为 1) 谁需要这个;和 2) memcpy 对此非常有效,无需挑选标准的狡猾解释来使其工作。
  • @R.MartinhoFernandes:回答你的问题,“谁需要这个,”我愿意。
  • @user2485710: sizeof 以字节为单位工作,因为所有对象大小都是字节的倍数。故事结局。现在请你停止这种毫无意义的争论。

标签: c++ unions reinterpret-cast


【解决方案1】:

与其他答案的状态相反,从实用的角度来看,存在巨大差异,尽管标准可能没有这样的差异。

从标准的角度来看,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;
}

[1] 中的实现得到了我所知道的所有编译器(gcc、clang、VS、sun、ibm、hp)的认可,而 [2] 中的实现没有并且将失败在其中一些使用积极优化时。特别是,我看到 gcc 在评估 ntohl 之前将指令重新排序并 读取dbl 变量中,从而产生错误的结果。


(*) 除了您始终可以读取 [signed|unsigned] char*,而不管真实对象(原始指针类型)是什么。

(+) 同样,除了一些例外,如果活动成员与另一个成员共享一个公共前缀,您可以通过 compatible 成员读取该前缀。

【讨论】:

  • 有趣。 reinterpret_cast-implementation 的失败让我感到惊讶。这可以被认为是一个错误,还是标准过于严格而无法在此类应用程序中使用强制转换?我不熟悉 C++ 中的网络。
  • @MarcClaesen:谷歌严格的别名和一些直言不讳的词,你会发现有人在 GCC 中抱怨这种行为。标准提供了一组有效别名(多个指针引用同一个对象)的情况,在有限的集合之外,其他都是未定义的行为,上面reinterpret_cast的情况是未定义的行为。欲了解更多信息,谷歌 'strict aliasing'、'no-strict-aliasing' 或类似的东西
  • 关于编译器如何实际处理这个问题的要点。在这种情况下,行为是实现定义的(实际上,一个理智的编译器如何定义这个,而不浪费周期来创建随机 UB?) - 老实说,在相关方面非常有用案例。
  • 我想看看reinterpret_cast 中断的示例(直接网址)。这两个函数生成相同的汇编器输出。
  • @MaximEgorushkin:第二个 sn-p 来自对我们产品的修复,当使用 -O2 编译时,没有 -fno-strict-aliasing 和 gcc(我相信当时是 4.3)优化器在读入int64_t 并运行bswap(对于htonll)之前,重新排序了写入double 的指令。我没有检查较新的版本。
【解决方案2】:

正确的union 和(假设)正确且安全的reinterpret_cast 之间存在一些技术差异。但是,我想不出任何无法克服的差异。

在我看来,更喜欢union 而不是reinterpret_cast真正原因不是技术原因。用于文档。

假设您正在设计一堆类来表示一个有线协议(我猜这是首先使用类型双关语的最常见原因),并且该有线协议由许多消息、子消息和字段组成。如果其中一些字段很常见,例如 msg 类型、seq# 等,则使用联合可以简化将这些元素绑定在一起的过程,并有助于准确记录协议在网络上的显示方式。

显然,使用reinterpret_cast 会做同样的事情,但是为了真正了解发生了什么,您必须检查从一个数据包前进到下一个数据包的代码。使用union,您只需查看标题即可了解发生了什么。

【讨论】:

  • @R.MartinhoFernandes:在我想出一个很好的答案来回答你的问题之前,我会从我的答案中删除那一点。
  • 很公平。 FWIW 我相信你弄错了:编译器保证联合中的对齐是正确的,而 reinterpret_cast 到具有更严格对齐的类型是危险的。
  • @R.MartinhoFernandes:不,我知道。
  • 由于此时没有出现“相关”差异,我将接受这个答案,以防止这个问题真的过时或在形而上学方面。
  • @R.MartinhoFernandes:该标准仅保证指针的往返转换:reinterpret_cast&lt;T*&gt;(reinterpret_cast&lt;U*&gt;(Tptr)),并且仅在 U 上的对齐约束弱于的前提下T。它不保证,例如:reinterpret_cast&lt;char*&gt;(reinterpret_cast&lt;double*&gt;(charp)),因为某些实现可能无法保存指向未对齐位置的 double*
【解决方案3】:

在 C++11 中,union 是 类类型,您可以使用非平凡的成员函数来持有成员。你不能简单地从一个成员转换到另一个成员。

§ 9.5.3

[ 示例:考虑以下联合:

union U {
int i;
float f;
std::string s;
};

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

【讨论】:

  • 好的,如果我决定 T 和 U 只是 POD 类型怎么办?在那种情况下,考虑到您的回答,您基本上是在说它们是相同的,对吗?
  • 您可能会补充说,联合确实是在不同时间包含不同类型对象的类,这与重新解释类型有着根本的不同。
  • @user2485710:它们是否是 POD 类型并不重要;就 C++ 而言,这要么是实现定义的行为,要么是 undefined 行为。仅当您访问存储在其中的变量时,联合对 C++ 才有意义。 C++ 没有用于就地类型双关的标准保护机制。
【解决方案4】:

从实际的角度来看,它们很可能是 100% 相同的,至少在真实的非虚构计算机上是这样。您将一种类型的二进制表示形式填充到另一种类型中。

从语言律师的角度来看,在某些情况下使用 reinterpret_cast 是明确定义的(例如指向整数转换的指针),而在其他情况下是特定于实现的。

另一方面,联合类型的双关语总是非常明显的未定义行为(尽管未定义不一定意味着“不起作用”)。该标准规定,最多一个非静态数据成员的值可以随时存储在联合中。这意味着如果您设置var1,则var1 有效,但var2 无效。
但是,由于var1var2 存储在相同的内存位置,您当然仍然可以根据需要读取和写入任何类型,并且假设它们具有相同的存储大小,不会“丢失”任何位。

【讨论】:

  • 这不仅仅是关于计算机。这也与编译器有关。
  • 你的最后两段完全相互矛盾。最后一个是高效编译器实现的实际结果,而不是标准定义的任何内容。由于阅读除了最近写入的成员之外的成员是 UB,一些理论上的 - 和糟糕的 - 编译器会很遗憾地返回完全随机的垃圾,而不是简单得多(并且实际上完成)的替代方案重新解释位模式。可以说你可以实际做到这一点,但在语言层面上并不能保证,所以需要明确限定
  • @underscore_d:这是未定义的主要原因是(除了在定义良好的 C 语言中)对象可能具有需要设置非平凡状态的构造函数/析构函数(即,和可能的填充,但这对于示例来说无关紧要)。如果有的话,编译器可以优化整个范围(这是废话,但不幸的是可能是真实的),而不是返回内存位置中发生的任何位。返回垃圾是对 UB 的一种变态解释,这与“允许格式化您的硬盘”属于同一类别。从来没有编译器...
  • ... 允许故意丢弃一些随意的内存位置只是为了“教”你,无论你是否调用了 UB。就语言标准而言,它不需要做任何特别的事情,但这并不意味着它被允许成为一个完整的家伙。如果您不写入,存储单元的二进制内容可能不会改变(除非宇宙辐射稍微翻转,等等)。在此范围内,这些段落并不相互矛盾。你可以做到,只是不能保证编译器会做任何事情
  • @underscore_d:编译器确实可以选择使用它仍然在寄存器中的结构成员的旧(但有效!)数值,这是完全正确的。这不仅是合法的,而且甚至是有道理的——毕竟,由于您没有修改该成员,因此定义的值 是相同的 (逻辑上,而不是事实上)。幸运的是,这种情况不太可能发生,需要在同一范围内连续进行几次这样的转换。
猜你喜欢
  • 2011-10-14
  • 1970-01-01
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
  • 1970-01-01
  • 2010-11-27
相关资源
最近更新 更多