问题 正确定义的union和reinterpret_cast之间有什么区别?


你能否提出至少1个场景,其中存在重大差异

union {
T var_1;
U var_2;
}

var_2 = reinterpret_cast<U> (var_1)

我对此的思考越多,它们对我来说就越相似,至少从实际的角度来看。

我发现的一个区别是虽然联合大小在大小方面是最大的数据类型,但是这篇文章中描述的reinterpret_cast可能会导致截断,所以普通的旧式C式联合比新的更安全C ++铸造。

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


901
2017-07-29 12:16


起源

据我所知,使用 union用于类型惩罚的s在C中是安全的 - 我不确定C ++,也许它不是,然后你必须使用类型转换。
@ H2CO3我不知道(老实说也不在乎)如果使用工会是安全的,但reinterpret_cast严格来说并不安全。 - R. Martinho Fernandes
@ user2485710因为1)地狱需要谁;和2) memcpy 对此非常好,不需要选择标准的黄鼠狼解释来使其工作。 - R. Martinho Fernandes
@ R.MartinhoFernandes:回答你的问题,“他到底需要这个,”我这样做。 - John Dibling
@ user2485710: sizeof 以字节为单位工作,因为所有对象大小都是字节的倍数。故事结局。现在请你停止这个毫无意义的论点。 - Mike Seymour


答案:


与其他答案所说的相反,来自a 实际的 观点存在巨大差异,虽然标准可能没有这样的差异。

从标准的角度来看, reinterpret_cast 只保证中间指针类型的对齐要求不高于源类型的对齐要求,才能保证只能用于往返转换。你没有权限 (*) 读取一个指针并从另一个指针类型读取。

同时,该标准需要来自联合的类似行为,从活动成员(最后写入的成员)之外读取联合成员是未定义的行为。(+)

然而编译器经常为union案例提供额外的保证,我所知道的所有编译器(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重新排序指令和  进入 dbl 评估前的变量 再用ntohl从而产生错误的结果。


(*) 除了你总是被允许  从一个 [signed|unsigned] char* 无论真实对象(原始指针类型)是什么。

(+) 除了一些例外,如果活动成员与另一个成员共享一个公共前缀,您可以通读 兼容 成员的前缀。


8
2017-07-29 13:24



有趣。失败了 reinterpret_cast - 实施令我惊讶。这可以被认为是一个错误,还是标准过于严格,无法使用演员表进行此类应用?我不熟悉C ++中的网络。 - Marc Claesen
@MarcClaesen:谷歌严格别名和一些直言不讳的话,你会发现有人在GCC中抱怨这种行为。该标准提供了一系列情境 有效的别名 (指向同一个对象的多个指针),在有限集之外,其他一切都是未定义的行为,以及的情况 reinterpret_cast以上是未定义的行为。有关详细信息,谷歌 '严格别名','无严格别名' 或类似的东西 - David Rodríguez - dribeas
关于编译器如何处理这个问题的优秀观点 几乎。在这种情况下,行为是实现定义的(实际上,还有其他方式) 将 一个理智的编译器定义这个,而不浪费周期来创建随机UB?) - 说实话,在相关情况下非常有用。 - underscore_d
我想看(一个直接的网址)一个例子 reinterpret_cast 休息。这两个函数生成相同的汇编程序输出。 - Maxim Egorushkin
@MaximEgorushkin:第二个片段来自对我们产品的修复,当编译时 -O2没有 -fno-strict-aliasing 和gcc(我相信它当时是4.3)优化器在读入之前重新排序写入双精度的指令 int64_t 并运行 bswap (对于 htonll)。我还没有检查过更新的版本。 - David Rodríguez - dribeas


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

真实 理由更喜欢 union 过度 reinterpret_cast 在我看来,这不是一个技术问题。这是为了文档。

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

运用 reinterpret_cast 显然,做同样的事情,但为了真正知道发生了什么,你必须检查从一个数据包前进到下一个数据包的代码。用一个 union 你可以看看标题,了解发生了什么。


5
2017-07-29 12:39



工会会出现哪些对齐问题? - R. Martinho Fernandes
@ R.MartinhoFernandes:直到我能想到你的问题的一个好的答案,我将从我的答案中删除那一点。 - John Dibling
很公平。 FWIW我相信你得到了错误的方法:联合中的对齐是由编译器保证的,而 reinterpret_cast 对于具有更严格对齐的类型是危险的。 - R. Martinho Fernandes
@ R.MartinhoFernandes:不,我知道。 - John Dibling
由于此时没有出现“相关”差异,我将接受这个答案,以防止这个问题真正发生在OT或形而上学方面。 - user2485710


在C ++ 11中,union是 班级类型,你可以拥有一个具有非平凡成员函数的成员。你不能简单地从一个成员转变为另一个成员。

§9.5.3

[示例:考虑以下联合:

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

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


1
2017-07-29 12:23



好吧,如果我认为T和U只是POD类型怎么办?在这种情况下,考虑到你的答案,你基本上说它们是相同的,对吧? - user2485710
您可以补充说,联合实际上是在不同时间包含不同类型对象的类,这与重新解释类型有根本的不同。 - cli_hlt
@ user2485710:它们是否是POD类型并不重要;就C ++而言,这是实现定义的行为或 未定义 行为。如果您访问存储在其中的变量,联合只对C ++有意义。 C ++没有用于就地类型双关语的标准保护机制。 - Nicol Bolas


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

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

另一方面,联盟类型的惩罚是非常明确的未定义行为,总是(虽然未定义并不一定意味着“不起作用”)。该标准规定,至多一个非静态数据成员的值可以随时存储在并集中。这意味着如果你设置了 var1 然后 var1 是有效的,但是 var2 不是。
但是,自从 var1 和 var2 存储在同一个内存位置,您当然可以根据需要读取和写入任何类型,并假设它们具有相同的存储大小,不会丢失任何位。


-1
2017-07-29 12:36



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