首页 文章

工会和打字

提问于
浏览
43

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

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

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

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

总结一下:

  • 为什么使用工会进行打字是不好的?

  • 如果不是这样,它们的意义何在?

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

5 回答

  • 9

    要重新迭代,通过联合进行类型惩罚在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进行评级,必须等到下一个标准修订才能得到修复 .


    GNU扩展到标准C(和C90)do explicitly allow type-punning with unions . 其他不属于基本语言标准的编译器 .

  • 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;
    }
    

    叫来:

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

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

    编辑:

    由于其他答案认为行为实际上是定义的(主要是基于引用非规范性说明,脱离背景):

    这里的正确答案是pablo1977:当涉及类型惩罚时,标准不会尝试定义行为 . 可能的原因是它没有可定义的可移植行为 . 这并不妨碍特定实现定义它;虽然我不记得有关这个问题的任何具体讨论,但我很确定意图是实现定义了一些东西(大多数,如果不是全部的话) .

    关于使用联合进行类型惩罚:当C委员会正在开发C90(在1980年代后期)时,有一个明确的意图允许调试实现进行额外的检查(例如使用胖指针进行边界检查) . 从当时的讨论中可以清楚地看出,调试实现可能会缓存有关联合中初始化的最后一个值的信息,如果您尝试访问其他任何内容则会陷阱 . 这在§6.7.2.1/ 16中有明确规定:“最多一个成员的 Value 可以随时存储在一个联盟对象中 . ”访问不存在未定义行为的值;它可以被同化为访问未初始化的变量 . (当时有一些讨论是否访问具有相同类型的不同成员是否合法 . 但我不知道最终解决方案是什么;在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;
        //  ...
    };
    

    并且合法访问base.type,即使节点是通过 inner 初始化的 . (事实上§6.5.2.3/ 6以"One special guarantee is made..."开头并继续明确允许这是一个非常强烈的迹象,表明所有其他情况都是未定义的行为 . 当然,还有声明“否则未定义的行为在本国际标准中用“未定义的行为”或“§4/ 2中省略任何明确的行为定义”一词表示;为了证明行为不是未定义的,你必须说明它的定义在标准中 . )

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

    int
    getExponent( double d )
    {
        return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
    }
    

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

  • 32

    工会最初的目的是为了节省空间,当你希望能够代表不同的类型时,我们称之为variant typeBoost.Variant就是一个很好的例子 .

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

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

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

    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,并且如果您的编译器没有生成相同的代码,那么该参数就应该被认为是一个bug:

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

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

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

  • 3

    它在C99中是合法的:

    来自标准: 6.5.2.3 Structure and union members

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

  • 2

    BRIEF ANSWER: Type punning 在某些情况下可以安全 . 另一方面,虽然它似乎是一个众所周知的做法,但标准似乎并不是非常感兴趣使它正式化 .

    我只会谈论 C (而不是C) .

    1. TYPE PUNNING and THE STANDARDS

    正如大家已经指出的那样,在标准C99和C11中允许 type punning ,在 6.5.2.3 小节中 . 但是,我会根据自己对问题的看法重写事实:

    • 标准文件C99和C11的 6.5 部分开发了 expressions 的主题 .

    • 小节 6.5.2 被称为 postfix expressions .

    • 子部分 6.5.2.3 谈到 structs and unions .

    • 段落 6.5.2.3(3) 解释了应用于 structunion 对象的 dot operator ,以及将获得的值 .
      就在那里, footnote 95 出现了 . 这个脚注说:

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

    type punning 几乎没有出现,作为脚注,它提供了一个线索,它不是C编程中的相关问题 .
    实际上, the main purpose for using unions is for saving space (在内存中) . 由于几个成员共享相同的地址,如果知道每个成员将使用程序的不同部分,从不同时使用 union 而不是 struct ,以节省内存 .

    • 提到了 6.2.6 小节 .

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

    2. REPRESENTATION OF TYPES and ITS TROUBLE

    如果你注意标准的不同方面,你几乎可以肯定:

    • 没有明确指定指针的表示 .

    • 最糟糕的是,具有不同类型的指针可能具有不同的表示形式(作为内存中的对象) .

    • union 成员在内存中共享相同的 Headers 地址,它与 union 对象本身的地址相同 .

    • struct 成员通过与 struct 对象本身完全相同的内存地址开始增加相对地址 . 但是,可以在每个成员的末尾添加填充字节 . 多少?这是不可预测的 . 填充字节主要用于存储器对齐目的 .

    • 算术类型(整数,浮点实数和复数)可以通过多种方式表示 . 这取决于实施 .

    • 特别是,整数类型可能有 padding bits . 我认为,台式电脑并非如此 . 然而,该标准为这种可能性打开了大门 . 填充比特用于特殊目的(奇偶校验,信号,谁知道),而不是用于保存数学值 .

    • signed 类型可以有3种表示方式:1 's complement, 2' s补码,只是符号位 .

    • char 类型只占用1个字节,但1个字节可以有不同的位数8(但绝不少于8) .

    • 但是我们可以确定一些细节:

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

    3. TYPE PUNNING vs TYPE REPRESENTATION

    所有这些观察结果都表明,如果我们尝试 type punningunion 成员的类型不同 unsigned char ,我们可能会有很多歧义 . 它不是可移植的代码,特别是我们可能会有不可预测的程序行为 .
    但是, the standard allows this kind of access .

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

    4. THE SAFE CASE: unsigned char

    使用 type punning 的唯一安全方式是 unsigned char 或井 unsigned char 数组(因为我们知道数组对象的成员是严格连续的,并且当用 sizeof() 计算它们的大小时没有任何填充字节) .

    union {
         TYPE data;
         unsigned char type_punning[sizeof(TYPE)];
      } xx;
    

    由于我们知道 unsigned char 以严格的二进制形式表示,没有填充位,因此可以使用类型双关语来查看成员 data 的二进制表示 .
    在特定实现中,此工具可用于分析给定类型的值的表示方式 .

    我无法在标准规范下看到 type punning 的另一个安全且有用的应用程序 .

    5. A COMMENT ABOUT CASTS...

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

    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
    

相关问题