工会和打字

Mat*_*ins 63 c c++ unions type-punning

我一直在寻找,但找不到明确的答案.

很多人说使用工会来打字 - 双关语是不明确的和不好的做法.为什么是这样?考虑到你写入原始信息的内存并不仅仅是自己的改变,我看不出为什么它会做任何未定义的任何原因(除非它超出了堆栈的范围,但这不是一个联合问题,这将是糟糕的设计).

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

如果不打双关语,联盟的意义又是什么呢?我在某个地方看到它们应该被用来在不同的时间使用相同的内存位置来获取不同的信息,但为什么不在再次使用之前删除信息呢?

总结一下:

  1. 为什么使用工会进行打字是不好的?
  2. 如果不是这样,他们的意义何在?

额外信息:我主要使用的是C++,但想了解它和C.特别是我正在使用工会在浮点数和原始十六进制之间进行转换以通过CAN总线发送.

Chr*_*oph 41

要重新迭代,通过联合进行类型惩罚在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)的GNU扩展明确允许使用联合进行类型惩罚.其他不支持GNU扩展的编译器也可能支持union type-punning,但它不是基本语言标准的一部分.

  • 脚注是非规范性的,在上下文中,显然是一个解释,因为委员会没有对此进行定义.它确实_not_定义行为. (4认同)
  • @JamesKanze:*该值是指定成员*的值.这是脚注所阐明的规范部分.如果构成该成员的对象表示的所有字节都采用指定的值并且不对应于陷阱表示,则该成员也将采用指定的值.这些字节是如何到达的(通过`memcpy`,通过`char*`修改,通过不同的联合成员,......)并不重要.否则你将无法说服我,所以除非你改变主意,否则继续下去可能毫无意义...... (4认同)
  • 我手边没有C90副本来验证背景; 我确实从委员会的讨论中记得,其中一个意图是措辞应该允许"调试"实现,如果访问不是最后写的元素,那么它将被捕获.(当然,这是在20世纪80年代后期; C委员会的态度可能已经发展了.)我似乎记得这是通过未定义的行为实现的,但实现定义也可以解决问题.(这里的主要区别在于需要实施来记录它的作用.) (2认同)

Sha*_*our 11

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

另一个常见的用法是类型惩罚,这是有争议的,但实际上大多数编译器支持它,我们可以看到gcc文档支持:

从不同的工会成员阅读的做法比最近写的那个(称为"打字式")很常见.即使使用-fstrict-aliasing,只要通过union类型访问内存,就允许类型为punning.因此,上面的代码按预期工作.

请注意,即使使用-fstrict-aliasing,也允许使用type-punning,这表示在播放时存在别名问题.

Pascal Cuoq认为缺陷报告283澄清了这一点在C中被允许.缺陷报告283添加了以下脚注作为澄清:

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

在C11中,这将是脚注95.

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

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

在C++中,尚不清楚是否定义了行为.

此讨论还涵盖了至少一个原因,即允许通过联合进行类型惩罚是不可取的:

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

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

std::int64_t n;
std::memcpy(&n, &d, sizeof d);
Run Code Online (Sandbox Code Playgroud)

而不是这个:

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

u1 u ;
u.d = d ;
Run Code Online (Sandbox Code Playgroud)

我们可以看到使用godbolt这会生成相同的代码,并且如果你的编译器没有生成相同的代码就会产生参数,它应该被认为是一个bug:

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

博客文章Type Punning,Strict Aliasing和Optimization也得出了类似的结论.

未定义的行为邮件列表讨论:键入双关语以避免复制涵盖了很多相同的基础,我们可以看到该区域的灰度.

  • 值得一提的是 `std::memcpy` 仅在类型是 TriviallyCopyable 时才有效 (3认同)
  • memcpy 生成相同代码的说法忽略了这样一个事实:如果编译器记录可以识别别名的模式,则可以生成更高效的代码,在这种情况下,编译器只需要制作少量的温和悲观(但可能是准确的)假设,而 memcpy 通常会迫使编译器做出更悲观的假设。memcpy 本身的代码看起来不错,但它对周围代码的影响并不大。 (2认同)

Dav*_*eri 6

它在C99中是合法的:

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

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

  • @JamesKanze我认为"这可能是一个陷阱表示"意味着如果新类型具有陷阱表示,那么在实现定义的条件下,类型惩罚的结果可能是其中之一. (7认同)
  • @JamesKanze你能否扩展一下如何"将值的对象表示的适当部分重新解释为6.2.6中描述的新类型中的对象表示(有时称为"类型双关").这可能是一个陷阱表示"是一种说它未定义的行为的奇特方式?在我看来,它所说的是对新类型的重新解释,并且这是一种奇特的方式来说它是**实现定义的**行为,如果有的话. (6认同)

Jam*_*nze 5

有(或者至少在 C90 中)有两种修改方式可以实现这种未定义的行为。第一个是允许编译器生成额外的代码来跟踪联合中的内容,并在您访问错误的成员时生成一个信号。在实践中,我认为没有人做过(也许是 CenterLine?)。另一个是这开启了优化的可能性,并使用了这些。我使用的编译器会将写入推迟到最后可能的时刻,理由是它可能没有必要(因为变量超出范围,或者随后写入了不同的值)。从逻辑上讲,人们会期望当联合可见时会关闭此优化,但在 Microsoft C 的最早版本中却没有。

类型双关的问题很复杂。C 委员会(早在 1980 年代后期)或多或少地认为您应该为此使用强制转换(在 C++ 中,reinterpret_cast),而不是联合,尽管这两种技术在当时都很普遍。从那时起,一些编译器(例如 g++)采取了相反的观点,支持使用联合,但不支持使用强制转换。在实践中,如果不是很明显存在类型双关,则两者都不起作用。这可能是 g++ 观点背后的动机。如果您访问联合成员,很明显可能存在类型双关。但是,当然,考虑到以下内容:

int f(const int* pi, double* pd)
{
    int results = *pi;
    *pd = 3.14159;
    return results;
}
Run Code Online (Sandbox Code Playgroud)

调用:

union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );
Run Code Online (Sandbox Code Playgroud)

根据标准的严格规则是完全合法的,但在 g++(可能还有许多其他编译器)上失败;编译时f,编译器假定pi 并且pd不能别名,并重新排序写入*pd和读取*pi. (我相信这从来不是保证这一点的意图。但标准的当前措辞确实保证了这一点。)

编辑:

由于其他答案认为行为实际上是定义的(主要基于引用非规范性注释,断章取义):

这里的正确答案是 pablo1977:当涉及类型双关时,标准没有尝试定义行为。可能的原因是没有它可以定义的可移植行为。这不会阻止特定实现定义它;尽管我不记得对该问题的任何具体讨论,但我很确定其意图是实现定义了一些东西(并且大多数,如果不是全部的话)。

关于使用联合进行类型双关:当 C 委员会开发 C90 时(在 1980 年代后期),有一个明确的意图是允许调试进行额外检查的实现(例如使用胖指针进行边界检查)。从当时的讨论来看,很明显其意图是调试实现可能会缓存有关联合中初始化的最后一个值的信息,并在您尝试访问其他任何内容时捕获。这在第 6.7.2.1/16 节中明确说明:“任何时候最多可以将一个成员的值存储在联合对象中。” 访问不存在的值是未定义的行为;它可以被同化为访问未初始化的变量。(当时有一些关于访问相同类型的不同成员是否合法的讨论。然而,我不知道最终的决议是什么;大约在 1990 年之后,我转向了 C++。)

关于 C89 的引用,说行为是实现定义的:在第 3 节(术语、定义和符号)中找到它似乎很奇怪。我必须在家里的 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;
    //  ...
};
Run Code Online (Sandbox Code Playgroud)

并且合法地访问 base.type,即使 Node 是通过inner. (第 6.5.2.3/6 节以“做出一个特殊保证......”开始并继续明确允许这一事实,这是一个非常强烈的迹象,表明所有其他情况都是未定义的行为。当然,还有是在 §4/2 中声明“在本国际标准中用‘未定义的行为’一词或通过省略任何明确的行为定义”来表示未定义的行为;为了论证该行为不是未定义的,您必须显示它在标准中的定义位置。)

最后,关于类型双关:所有(或至少所有我使用过的)实现都以某种方式支持它。我当时的印象是,意图是指针转换是实现支持它的方式;在 C++ 标准中,甚至有(非规范性的)文本表明,对于reinterpret_cast熟悉底层架构的人来说,a 的结果“不足为奇”。然而,在实践中,如果访问是通过联合成员进行的,则大多数实现支持使用联合进行类型双关。大多数实现(但不是 g++)也支持指针转换,前提是指针转换对编译器清晰可见(对于某些未指定的指针转换定义)。底层硬件的“标准化”意味着:

int
getExponent( double d )
{
    return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}
Run Code Online (Sandbox Code Playgroud)

实际上相当便携。(当然,它在大型机上不起作用。)不起作用的是我的第一个示例,其中别名对编译器不可见。(我很确定这是标准中的一个缺陷。我似乎记得甚至看过一个关于它的 DR。)

  • 事实上,C 委员会通过引入有效类型,将指针强制转换用于类型双关是非法的,所以使用联合是 C 的方式 (4认同)
  • 它是*实现定义的*,而不是 C90 中的 *未定义* - 使其非法是 C++ 主义 (3认同)
  • @Christoph 在 C11 中仍然是未定义的行为,至少在我拥有的副本中是这样。§6.7.2.1/16 对此非常清楚。C++ 甚至更清楚,因为它具有与存储持续时间分开的对象生存期的概念,但即使在 C 中,访问未初始化的对象(而不是作为字节序列)也是未定义的行为,并分配给联合的一个元素使所有其他人“未初始化”。 (2认同)