工会和打字
我一直在寻找一段时间,但找不到明确的答案。
很多人说使用工会来打字是不明确的,也是不好的做法。 为什么是这样? 我不明白为什么它会做任何未定义的事情,考虑到你写的原始信息的内存不会只是自己改变(除非它超出了堆栈的范围,但这不是一个工会的问题,这将是不好的devise)。
人们引用严格的走样规则,但在我看来,就像说你做不到,因为你做不到。
如果不打字双方又有什么关系呢? 我看到某个地方他们应该被用来在不同的时间使用相同的内存位置来获取不同的信息,但是为什么不在删除信息之前再次使用呢?
总结:
- 为什么使用工会来打字?
- 如果不是这个,他们的意义何在?
额外的信息:我主要使用C ++,但想知道这一点和C.具体来说,我使用工会之间转换浮动和原始hex通过CAN总线发送。
重新迭代,通过联合打字是完全正确的C(但不是在C + +)。 相比之下,使用指针强制转换违反了C99严格的锯齿,因为不同的types可能有不同的alignment要求,所以如果做错了,可以引发一个SIGBUS。 与工会,这不是一个问题。
C标准的相关引用是:
C89第3.3.2.3节§5:
如果一个联合对象的成员在一个值被存储在该对象的不同成员之后被访问,则该行为是实现定义的
C11第6.5.2.3节§3:
后缀expression式后跟。 运算符和标识符指定结构或联合对象的成员。 值是指定成员的值
用以下脚注95:
如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不相同,则该值的对象表示forms的适当部分将被重新解释为新types中的对象表示forms,如如6.2.6所述(一个有时称为“types双关”的过程)。 这可能是一个陷阱代表。
这应该是完全清楚的。
詹姆斯感到困惑,因为C11节6.7.2.1§16读取
最多一个成员的值可以随时存储在一个联合对象中。
这似乎是矛盾的,但它不是:与C ++相反,在C中,没有活动成员的概念,通过不兼容types的expression式访问单个存储的值是完全正确的。
另见C11附件J.1§1:
对应于除最后一个存储到[未指定]的联合成员的字节值。
在C99中,这用于阅读
除了最后一个存储到[未指定]的联合成员的值,
这是不正确的。 由于附件不规范,没有对自己的技术评分进行评分,只能等到下一个标准修订才能确定。
对标准C ++(和C90)的GNU扩展明确允许使用联合进行types双击 。 其他不支持GNU扩展的编译器也可能支持联合打字,但它不是基本语言标准的一部分。
联盟最初的目的是为了节省空间,当你想能够代表不同的types,我们称之为变种types,看到Boost.Variant就是一个很好的例子。
另一个常见的用法就是types戳这个有效性是有争议的,但实际上大多数编译器都支持它,我们可以看到gcc文档支持 :
从不同的工会会员阅读的做法比最近写的(称为“打字”)是常见的。 即使使用-fstrict-aliasing,只要内存是通过联合types访问的,则允许使用types双击。 所以,上面的代码按预期工作。
请注意, 即使使用-fstrict-aliasing,也允许使用types双击来表示游戏中存在别名问题。
Pascal Cuoq认为缺陷报告283澄清了这是允许的C. 缺陷报告283增加了以下脚注作为澄清:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不相同,则该值的对象表示的相应部分将被重新解释为新types中的对象表示forms,如如6.2.6所述(一个有时称为“types双关”的过程)。 这可能是一个陷阱代表。
在C11中将是脚注95
。
虽然在std-discussion
邮件组主题types穿越联盟的论点是这样的,这是不明确的,这似乎是合理的,因为DR 283
没有增加新的规范措辞,只是一个脚注:
在我看来,这是一个未明确规定的C语义泥潭,在实施者和C委员会之间还没有达成一致意见,究竟哪些案件有明确的行为,
在C ++中,不清楚是否定义了行为 。
这个讨论还至less包含了一个为什么允许通过一个工会打字的理由是不可取的:
C标准的规则打破了当前实现所执行的基于types的别名分析优化。
它打破了一些优化。 对此的第二个参数是使用memcpy应该生成相同的代码,并且不会中断优化和定义良好的行为,例如:
std::int64_t n; std::memcpy(&n, &d, sizeof d);
而不是这个:
union u1 { std::int64_t n; double d ; } ; u1 u ; ud = d ;
我们可以看到使用godbolt这会产生相同的代码,并且如果你的编译器不能生成相同的代码,它会被认为是一个错误:
如果你的实现是这样,我build议你提交一个bug。 打破真正的优化(基于基于types的别名分析的任何东西),以解决一些特定编译器的性能问题似乎对我来说是一个坏主意。
博客文章types击穿,严格的别名和优化也得出了类似的结论。
未定义的行为邮件列表讨论: 键入双击以避免复制涵盖了很多相同的地面,我们可以看到这个领土是多么的灰暗。
在C99中是合法的:
从标准: 6.5.2.3结构和工会成员
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不相同,则该值的对象表示的相应部分将被重新解释为新types中的对象表示forms,如如6.2.6所述(一个有时称为“types双关”的过程)。 这可能是一个陷阱代表。
简单回答:在less数情况下, types剔除是安全的。 另一方面,虽然这似乎是一个非常知名的做法,但似乎标准并不是很有兴趣使之成为官方的。
我只会谈论C (而不是C ++)。
types打击和标准
正如人们已经指出的那样,在标准的C99和C11中,允许types双击在6.5.2.3中 。 但是,我会用我自己对这个问题的看法来重写事实:
- 标准文件C99和C11的6.5节开发了expression的主题。
- 6.5.2节被称为后缀expression式 。
- 第6.5.2.3 小节讨论结构和工会 。
- 第6.5.2.3(3)段解释了应用于
struct
或union
对象的点运算符 ,以及将获得哪个值。
就在那里, 脚注95出现。 这个脚注说:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不相同,则该值的对象表示的相应部分将被重新解释为新types中的对象表示forms,如如6.2.6所述(一个有时称为“types双关”的过程)。 这可能是一个陷阱代表。
types打点几乎没有出现的事实,作为一个脚注,它提供了一个线索,这不是在C编程相关的问题。
实际上, 使用unions
的主要目的是节省空间 (在内存中)。 由于多个成员共享相同的地址,如果知道每个成员将使用程序的不同部分,而不是在同一时间,那么可以使用union
而不是struct
来保存内存。
- 提到6.2.6小节。
- 第6.2.6小节讨论如何表示对象(例如在内存中)。
2.types的表示及其故障
如果你注意标准的不同方面,你几乎可以肯定没有什么:
- 指针的表示没有明确规定。
- 最糟糕的是,具有不同types的指针可能有不同的表示forms(如内存中的对象)。
-
union
成员在内存中共享相同的标题地址,它与union
对象本身的地址相同。 -
struct
成员具有增加的相对地址,通过开始在struct
对象本身相同的内存地址。 但是,可以在每个成员的末尾添加填充字节。 多less? 这是不可预测的。 填充字节主要用于内存分配目的。 - 算术types(整数,浮点实数和复数)可以用多种方式来表示。 这取决于实施。
- 特别是,整数types可以有填充位 。 对于台式电脑,我相信这不是事实。 然而,标准为这种可能性敞开了大门。 填充比特用于专门用途(奇偶校验,信号,谁知道),而不是保存math值。
-
signed
types可以有3种表示方式:1的补码,2的补码,只是符号位。 -
char
types只占用1个字节,但1个字节可以有不同的8个位(但从不less于8个)。 -
不过,我们可以确定一些细节:
一个。
char
types没有填充位。
湾unsigned
整数types完全按照二进制forms表示。
C。unsigned char
恰好占用1个字节,没有填充位,并且没有任何陷阱表示,因为使用了所有的位。 而且,它表示一个没有歧义的值,遵循整数的二进制格式。
3.types打击与types表示
所有这些观察结果都表明,如果我们试图用具有不同types的unsigned char
union
成员来打字 ,我们可能会有很多模糊性。 这不是可移植的代码,特别是我们的程序可能会有不可预知的行为。
但是, 标准允许这种访问 。
即使我们确信每种types都在我们的实现中performance出来,我们也可以拥有一系列在其他types( 陷阱表示 )中完全没有任何意义的位。 在这种情况下,我们无能为力。
4.安全CASE:unsigned char
使用types双关的唯一安全方式是使用unsigned char
或unsigned char
数组(因为我们知道数组对象的成员是严格连续的,并且在使用sizeof()
计算它们的大小时没有任何填充字节)。
union { TYPE data; unsigned char type_punning[sizeof(TYPE)]; } xx;
既然我们知道unsigned char
是以严格的二进制forms表示的,没有填充位,那么可以使用types双引号来查看成员data
的二进制表示forms。
这个工具可以用来分析给定types的值如何表示,在一个特定的实现。
在标准规范下,我无法看到另一种安全有用的打字types的应用程序。
5.关于CAST的评论…
如果你想玩types,最好是定义你自己的转换函数,或者只是使用types转换 。 我们可以记住这个简单的例子:
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
(或者至less在C90中)有两个修改是用来做出这个不确定的行为的。 首先是允许编译器生成额外的代码来跟踪联合中的内容,并在访问错误的成员时生成一个信号。 实际上,我从来没有想过(也许CenterLine?)。 另一个是开放的优化可能性,这些都被使用。 我使用了编译器,它会推迟写入,直到最后一个可能的时刻,因为它可能不是必需的(因为variables超出了范围,或者随后写入了不同的值)。 从逻辑上讲,人们会期望当联合可见时这个优化会被closures,但是它并不是最早的Microsoft C版本。
打字types的问题很复杂。 C委员会(早在二十世纪八十年代后期)或多或less采取了这样的立场,你应该使用cast(用C ++,reinterpret_cast),而不是工会,尽pipe这两种技术当时都很普遍。 从那时起,一些编译器(例如g ++)采取了相反的观点,支持使用联合,而不是使用强制转换。 而且在实践中,如果不是立即显而易见的就是打字。 这可能是g ++的观点背后的动机。 如果你访问一个工会成员,很明显可能会有打字的情况。 但是,当然,给一些像:
int f(const int* pi, double* pd) { int results = *pi; *pd = 3.14159; return results; }
所谓:
union U { int i; double d; }; U u; ui = 1; std::cout << f( &u.i, &u.d );
按照标准的严格规则是完全合法的,但是在g ++(可能还有许多其他编译器)中失败了。 编译f
,编译器假定pi
和pd
不能混叠,并将写入重新sorting到*pd
和从*pi
读取。 (我相信从来没有这个保证的意图,但目前的标准措辞确实保证了。)
编辑:
由于其他答案认为这种行为实际上是被定义的(主要是基于引用一个不符合规范的说明,而不是从上下文中去掉):
这里的正确答案是pablo1977:标准没有尝试定义涉及types双关的行为。 可能的原因是没有可以定义的便携式行为。 这并不妨碍具体的实现来定义它; 虽然我不记得有关这个问题的任何具体的讨论,但我确定这个意图是实现定义了一些东西(并且大部分,如果不是全部的话)。
关于使用工会打字的问题:当C委员会开发C90时(在20世纪80年代后期),明确的意图是允许进行额外检查的debugging实现(例如使用胖指针进行边界检查)。 从当时的讨论中可以清楚地看到,debugging实现可能会caching关于联合中初始化的最后一个值的信息,如果尝试访问其他任何东西,则会陷入陷阱。 这在第6.7.2.1 / 16节中有明确规定:“最多一个成员的值可以随时存储在一个联合对象中。” 访问不存在的值是未定义的行为; 它可以被同化访问一个未初始化的variables。 (当时有一些讨论,是否接触同一types的其他成员是否合法,但是我不知道最终的解决scheme是什么,在1990年左右之后,我转向了C ++)。
关于C89的引用,说这个行为是实现定义的:在第3节(术语,定义和符号)中find它似乎很奇怪。 我必须在家里查看C90的副本。 在标准的后续版本中被删除的事实表明它的存在被委员会认为是错误的。
标准支持的联合的使用是作为模拟推导的手段。 你可以定义:
struct NodeBase { enum NodeType type; }; struct InnerNode { enum NodeType type; NodeBase* left; NodeBase* right; }; struct ConstantNode { enum NodeType type; double value; }; // ... union Node { struct NodeBase base; struct InnerNode inner; struct ConstantNode constant; // ... };
并合法访问base.type,即使Node是通过inner
初始化的。 (第6.5.2.3 / 6节以“一个特殊的保证是……”开始的事实,并且明确地允许这个事实是一个非常强烈的表示,即所有其他情况都意味着未定义的行为。当然,那里是这样的陈述:“在本国际标准中未定义的行为以'未定义的行为'或者在第4/2节中省略了对行为的任何明确的定义 ';为了争辩行为不是未定义的,你必须显示它在标准中定义的位置。)
最后,关于type-punning:所有(或者至less我所用的所有)实现都以某种方式支持它。 我当时的印象是,这个意图是指针投射是实现支持它的方式; 在C ++标准中甚至有(非规范的)文本暗示reinterpret_cast
的结果对于熟悉底层架构的人来说是“不足为奇”的。 然而,实际上,大多数实现都支持使用联合进行types双击,只要通过联合成员进行访问即可。 如果指针转换对编译器来说是清晰可见的(对于指针转换的某些未指定的定义),大多数实现(但不是g ++)也支持指针转换。 而底层硬件的“标准化”则意味着:
int getExponent( double d ) { return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023; }
实际上是相当便携的。 (当然,它不会在大型机上工作。)什么不起作用就像我的第一个例子,其中别名是编译器不可见的。 (我很确定这是标准中的一个缺陷,我似乎还记得,甚至看到了关于它的DR。)