工会和打字

时间:2014-09-04 11:56:08

标签: c++ c unions type-punning

我一直在搜索,但无法找到明确的答案。

很多人说使用工会来打字 - 双关语是不明确的,也是不好的做法。为什么是这样?考虑到你编写原始信息的内存不能自行改变(除非它超出了堆栈的范围,但是&那个&),我无法看到为什么它会做任何未定义的任何原因。 #39;不是工会问题,这将是糟糕的设计)。

人们引用严格的别名规则,但在我看来,就像说你不能这样做,因为你无法做到。

如果没有输入双关语,联盟还有什么意义?我在某个地方看到他们应该习惯在不同的时间使用相同的内存位置来获取不同的信息,但为什么不在再次使用之前删除信息呢?

总结:

  1. 为什么使用工会进行打字是不好的?
  2. 如果不是这样,他们的意思是什么?
  3. 额外信息:我主要使用C ++,但想知道这一点和C.特别是我使用联合转换浮点数和原始十六进制来通过CAN总线发送。

4 个答案:

答案 0 :(得分:36)

要重新迭代,通过联合进行类型惩罚在C中完全没问题(但不是在C ++中)。相比之下,使用指针强制转换会违反C99严格别名并且存在问题,因为不同的类型可能有不同的对齐要求,如果做错了可以引发SIGBUS。有了工会,这绝不是问题。

C标准的相关引用是:

C89第3.3.2.3节§5:

  

如果在将值存储在对象的不同成员中之后访问union对象的成员,则该行为是实现定义的

C11第6.5.2.3节§3:

  

后缀表达式后跟。运算符和标识符指定结构或联合对象的成员。值是指定成员的值

以下脚注95:

  

如果用于读取union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将被重新解释为对象表示形式。 6.2.6中描述的新类型(有时称为''punning''的过程)。这可能是陷阱表示。

这应该非常清楚。


詹姆斯感到困惑,因为C11第6.7.2.1节§16读到

  

最多一个成员的值可以随时存储在一个union对象中。

这似乎是矛盾的,但事实并非如此:与C ++相比,在C中,没有活动成员的概念,通过不兼容类型的表达式访问单个存储值非常好。

另见C11附件J.1§1:

  

对应于最后存储到[未指定]的联合成员之外的联合成员的字节值。

在C99中,这用于阅读

  

存储在[未指定]

中的最后一个以外的工会成员的值

这是不正确的。由于附件不是规范性的,它没有对自己的TC进行评级,必须等到下一个标准修订才能得到修复。


标准C ++(和C90)do explicitly allow type-punning with unions的GNU扩展。其他不支持GNU扩展的编译器也可能支持union type-punning,但它不是基本语言标准的一部分。

答案 1 :(得分:10)

工会的最初目的是为了在您希望能够代表不同类型时节省空间,我们称之为variant type,请参阅Boost.Variant作为一个很好的例子。

另一个常见用途是type punning这个问题的有效性存在争议,但实际上大多数编译器都支持它,我们可以看到gcc documents its support

  

从不同的工会成员阅读的做法比最近写的那个(称为“打字式”)很常见。即使使用-fstrict-aliasing,也允许使用类型 - 双关语,前提是通过联合类型访问内存。因此,上面的代码按预期工作。

请注意,即使使用-fstrict-aliasing也会显示,允许使用类型标记,这表示在播放时存在别名问题。

Pascal Cuoq认为defect report 283澄清了这是允许的C. Defect report 283添加了以下脚注作为澄清:

  

如果用于访问union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将被重新解释为对象表示形式。 6.2.6中描述的新类型(有时称为“类型双关”的过程)。这可能是陷阱表示。

C11中的

将是脚注95

虽然在std-discussion邮件组主题Type Punning via a Union中提出了这个论点,但是这一点是不明确的,这似乎是合理的,因为DR 283没有添加新的规范性措辞,只是一个脚注:

  

在我看来,这是C中一个未指明的语义泥潭。   实施者与C之间尚未达成共识   委员会,确切地说明了哪些案例确定了行为,哪些案   不[...]

在C ++中it is unclear whether is defined behavior or not

这个讨论还包括至少一个原因,即允许通过联合进行打字是不可取的:

  

[...] C标准的规则打破了基于类型的别名   当前实现执行的分析优化。

它打破了一些优化。反对这一点的第二个论点是,使用memcpy应该生成相同的代码,并且不会破坏优化和明确定义的行为,例如:

std::int64_t n;
std::memcpy(&n, &d, sizeof d);

而不是:

union u1
{
  std::int64_t n;
  double d ;
} ;

u1 u ;
u.d = d ;

我们可以看到using godbolt this does generate identical code,并且如果您的编译器没有生成相同的代码,那么该参数就应该被认为是一个错误:

  

如果您的实施情况属实,我建议您提交一个错误。打破真正的优化(任何基于类型的别名分析)以解决某些特定编译器的性能问题对我来说似乎是一个坏主意。

博文Type Punning, Strict Aliasing, and Optimization也得出了类似的结论。

未定义的行为邮件列表讨论:Type punning to avoid copying涵盖了很多相同的基础,我们可以看到该领域的灰色程度。

答案 2 :(得分:5)

它在C99中是合法的:

从标准: 6.5.2.3结构和工会成员

  

如果用于访问union对象内容的成员不是   与上次用于在对象中存储值的成员相同,   值的对象表示的适当部分是   如上所述,重新解释为新类型中的对象表示   在6.2.6(一个过程有时被称为"类型双关语")。这可能是一个   陷阱表示。

答案 3 :(得分:3)

简要回答: 类型惩罚在某些情况下可以是安全的。另一方面,虽然它似乎是一个众所周知的做法,但似乎标准对于使它正式化并不是很感兴趣。

我只会谈论 C (不是C ++)。

<强> 1。打字类型和标准

正如大家已经指出的那样,在标准C99和C11中, 6.5.2.3 小节中允许类型双关语。但是,我会用我自己对这个问题的看法重写事实:

  • 标准文档C99和C11的 6.5 部分开发了表达式的主题。
  • 6.5.2 小节称为后缀表达
  • 小组 6.5.2.3 讨论结构和联合
  • 段落 6.5.2.3(3)解释了应用于structunion对象的点运算符,以及哪个值将是获得。
     就在那里,脚注95 出现了。这个脚注说:
  

如果用于访问union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将被重新解释为对象表示形式。 6.2.6中描述的新类型(有时称为&#34;类型惩罚&#34;)。这可能是陷阱表示。

类型双关语几乎不会出现这一事实,作为一个脚注,它提供了一个线索,表明它不是C编程中的相关问题。
实际上,使用unions的主要目的是节省空间(在内存中)。由于几个成员共享相同的地址,如果一个人知道每个成员将使用该程序的不同部分,从不同时使用union可以使用struct来保存记忆。

  • 提到 6.2.6 小节。
  • 6.2.6 小节讨论了如何表示对象(例如在内存中)。

<强> 2。类型的表示及其疑难

如果你注意标准的不同方面,你几乎什么都不能确定:

  • 指针的表示没有明确指定。
  • 最糟糕的是,具有不同类型的指针可能具有不同的表示形式(作为内存中的对象)。
  • union成员在内存中共享相同的标题地址,它与union对象本身的地址相同。
  • struct成员通过与struct对象本身完全相同的内存地址开始增加相对地址。但是,可以在每个成员的末尾添加填充字节。多少?这是不可预测的。填充字节主要用于存储器对齐目的。
  • 算术类型(整数,浮点实数和复数)可以通过多种方式表示。这取决于实施。
  • 特别是,整数类型可以有填充位。我相信,台式电脑不是这样。然而,该标准为这种可能性打开了大门。填充比特用于特殊目的(奇偶校验,信号,谁知道),而不是用于保存数学值。
  • signed类型可以有3种表示方式:1&是补码,2&是补码,只是符号位。
  • char类型只占用1个字节,但1个字节可以有8个不同的位数(但不能少于8个)。
  • 但是我们可以确定一些细节:

    一个。 char类型没有填充位  湾unsigned整数类型的表示形式与二进制形式完全相同  C。 unsigned char正好占用1个字节,没有填充位,并且没有任何陷阱表示,因为所有位都被使用。此外,它遵循整数的二进制格式表示没有任何歧义的值。

第3。 TYPE PUNNING与TYPE REPRESENTATION

所有这些观察结果都表明,如果我们尝试与union成员进行<{>>类型双关语,而unsigned char成员的类型不同unsigned char,我们可能会有很多歧义。它不是可移植的代码,特别是我们的程序可能会有不可预知的行为 但是,标准允许此类访问

即使我们确定在我们的实现中表示每种类型的具体方式,我们也可以在其他类型(陷阱表示)中具有一系列的含义。在这种情况下我们无能为力。

<强> 4。安全案例:未签名的字符

使用类型双关语的唯一安全方式是使用unsigned char或良好sizeof()数组(因为我们知道数组对象的成员是严格连续的并且没有任何数组当用 union { TYPE data; unsigned char type_punning[sizeof(TYPE)]; } xx; 计算它们的大小时填充字节。

unsigned char

由于我们知道data以严格的二进制形式表示,没有填充位,因此可以使用类型双关语来查看成员 union { unsigned char x; double t; } uu; bool result; uu.x = 7; (uu.t == 7.0)? result = true: result = false; // You can bet that result == false uu.t = (double)(uu.x); (uu.t == 7.0)? result = true: result = false; // result == true 的二进制表示。
在特定实现中,此工具可用于分析给定类型的值的表示方式。

我无法在标准规范下看到类型双关语<的另一个安全且有用的应用程序。

<强> 5。关于案例的评论......

如果想要使用类型,最好定义自己的转换函数,或者只使用强制转换。我们记得这个简单的例子:

{{1}}