C语言 联管节和打字双关

new9mtju  于 2023-02-21  发布在  其他
关注(0)|答案(5)|浏览(139)

我已经找了一段时间了,但是找不到一个明确的答案。
很多人说使用联合体来进行类型双关是未定义的,也是不好的做法。为什么呢?我看不出有什么理由它会做任何未定义的事情,考虑到你写入原始信息的内存不会自己改变(除非它超出了堆栈的范围,但这不是联合体的问题,这将是糟糕的设计)。
人们引用了严格的别名规则,但在我看来,这就像是说你做不到,因为你做不到。
另外,如果不输入双关语,联合还有什么意义呢?我在某个地方看到,它们应该被用来在不同的时间为不同的信息使用相同的内存位置,但为什么不在再次使用之前删除这些信息呢?
总结如下:
1.为什么在类型双关中使用联合是不好的?
1.如果不是这样,他们还有什么意义?
额外信息:我使用的主要是C++,但想知道关于这一点和C。具体来说,我正在使用联合之间的浮点数和原始十六进制转换发送通过CAN总线。

1hdlvixo

1hdlvixo1#

再重复一遍,在C中通过联合体进行类型双关是非常好的(但在C中不行)。相反,使用指针强制转换这样做违反了C99严格的别名,并且会有问题,因为不同的类型可能有不同的对齐要求,如果你做错了,你可能会引发SIGBUS。对于联合体,这永远不是问题。
C标准的相关引文如下:
第C89节3.3.2.3§5:
如果在将值存储在联合对象的其他成员中之后访问该对象的成员,则行为是实现定义的
第C11节6.5.2.3§3:
后缀表达式后跟.运算符和标识符,用于指定结构或联合对象的成员。该值是指定成员的值
加上以下脚注95:
如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则值的对象表示的适当部分被重新解释为新类型中的对象表示,如6.2.6中所述(该过程有时称为“类型双关”)。这可能是陷阱表示。
这应该是非常清楚的。
James感到困惑,因为C11部分6.7.2.1§16内容如下
在任何时候,最多一个成员的值可以存储在联合对象中。
这似乎是矛盾的,但事实并非如此:与C
不同,在C中,没有活动成员的概念,通过不兼容类型的表达式访问单个存储值是完全可以的。
另见C11附件J.1 §1:
未指定与上次存储到[中的联合成员以外的联合成员对应的字节的值。
在C99中,这曾经是
未指定存储到[中的最后一个联合成员以外的联合成员的值
这是不正确的。由于附录不是规范性的,它没有评定自己的TC,必须等到下一个标准修订版才能得到修复。
GNU对标准C++(和C90)的扩展确实明确地允许联合类型双关。其他不支持GNU扩展的编译器也可能支持联合类型双关,但它不是基础语言标准的一部分。

cvxl0en2

cvxl0en22#

联合体最初的目的是为了保存空间,当你想能够表示不同的类型时,我们称之为variant type,请看Boost.Variant作为一个很好的例子。
另一个常见的用法是type punning,其有效性还存在争议,但实际上大多数编译器都支持它,我们可以看到gcc记录了它的支持:
阅读与最近写入的联合体成员不同的联合体成员(称为“类型双关”)的做法很常见。即使使用-fstrict-aliasing,也允许类型双关,前提是内存是通过联合体类型访问的。因此,上面的代码按预期工作。
注意,它说 * 即使使用-fstrict-aliasing,也允许类型双关 *,这表明存在别名问题。
Pascal Cuoq认为缺陷报告283澄清了这在C. Defect report 283中是允许的,添加了以下脚注作为澄清:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则值的对象表示的适当部分被重新解释为新类型中的对象表示,如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:
如果您的实现确实如此,我建议您提交一个bug。打破真实的的优化(任何基于类型别名分析的优化)来解决某个特定编译器的性能问题对我来说似乎是个坏主意。
博客文章Type Punning, Strict Aliasing, and Optimization也得出了类似的结论。
未定义行为邮件列表讨论:Type punning to avoid copying覆盖了很多相同的领域,我们可以看到这个领域可以有多灰色。

9wbgstp7

9wbgstp73#

有(或者至少在C90中曾经是)两种修改来实现这种未定义的行为。第一种是允许编译器生成额外的代码来跟踪联合体中的内容,并在访问错误的成员时生成信号。实际上,我认为没有人这样做过(也许是中心线?)。另一个是优化的可能性,这打开了,这些是使用。我使用过编译器,将推迟写入,直到最后可能的时刻,理由是它可能不必要(因为变量超出了作用域,或者随后写入了不同的值)。逻辑上,当联合体可见时,应该会关闭此优化,但在Microsoft C的早期版本中没有。
类型双关的问题是复杂的。(早在20世纪80年代末)或多或少采取了应该使用强制类型转换的立场(在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:当涉及类型双关时,标准没有试图定义行为。2可能的原因是没有可移植的行为可以定义。3这并不妨碍特定的实现定义它;尽管我不记得对这个问题有过任何具体的讨论,但我非常确定其意图是实现定义一些东西(如果不是全部,也是大多数)。
关于使用联合进行类型双关:当时C委员会正在开发C90(在20世纪80年代后期),有一个明确的意图是允许调试执行附加检查的实现(比如使用胖指针进行边界检查)从当时的讨论中可以清楚地看出,其意图是调试实现可以缓存关于联合体中初始化的最后一个值的信息,如果你试图访问其他内容,则设置陷阱。这在§ 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;
    //  ...
};

并且合法地访问base.type,即使节点是通过inner初始化的。(§ 6.5.2.3/6以"做出一项特殊保证......"开始,然后明确允许这种行为,这一事实非常有力地表明,所有其他情况都是未定义的行为。§ 4/2中有"未定义的行为在本国际标准中以"未定义的行为"或 * 省略行为的任何明确定义 * 的方式表示"的声明;为了证明行为不是未定义的,你必须指出它在标准中的定义位置。)

最后,关于类型双关:所有的(或者至少是我用过的)实现都在某种程度上支持它,我当时的印象是指针转换的意图是实现支持它的方式;在C标准中,甚至有(非规范的)文本建议reinterpret_cast的结果对于熟悉底层体系结构的人来说是“不足为奇的”。然而,在实践中,大多数实现支持使用联合进行类型双关,只要访问是通过联合成员进行的。大多数实现(但不包括g)还支持指针强制转换,前提是编译器可以清楚地看到指针强制转换(对于某些未指定的指针强制转换定义)。底层硬件的“标准化”意味着:

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

实际上是相当可移植的。(当然,它在大型机上不起作用。)不起作用的是像我的第一个例子那样的东西,在那里别名对编译器是不可见的。(我非常肯定这是标准中的一个缺陷。我似乎记得甚至看到过与此相关的DR。)

m528fe3b

m528fe3b4#

在C99中是法律的的:
根据标准:6.5.2.3结构和工会成员
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则值的对象表示的适当部分被重新解释为新类型中的对象表示,如6.2.6中所述(该过程有时称为“类型双关”)。这可能是陷阱表示。

fae0ux8s

fae0ux8s5#

    • 简要回答:****类型双关语**在少数情况下可能是安全的。另一方面,尽管这似乎是一种非常众所周知的做法,但Standard似乎对使其正式化不是很感兴趣。

我将只讨论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。该脚注说:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则值的对象表示的适当部分被重新解释为新类型中的对象表示,如6.2.6中所述(该过程有时称为"类型双关")。这可能是陷阱表示。
事实上,类型双关语几乎没有出现,作为一个脚注,它给出了一个线索,这不是一个相关的问题在C编程。
实际上,使用unions的主要目的是节省空间(内存中)。由于几个成员共享同一地址,如果知道每个成员将被程序的不同部分使用,而不会同时使用,那么可以使用union代替struct,以节省内存。

  • 提到了第6.2.6小节。
  • 小节6.2.6讨论了对象是如何表示的(比如在内存中)。
    • 2.类型表示及其故障**

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

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

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

    • 3.类型碰撞与类型表示**

所有这些观察结果都表明,如果我们尝试对union成员的类型与unsigned char不同进行类型双关,我们可能会有很多歧义,这不是可移植的代码,特别是,我们可能会有不可预测的程序行为。
但是,标准允许这种访问
即使我们确信每种类型在我们的实现中都是以特定的方式表示的,我们也可能有一个在其他类型中毫无意义的位序列(陷阱表示)。在这种情况下,我们什么也做不了。

    • 4.保险箱:无符号字符**

使用类型双关语的唯一安全方式是使用unsigned char或well unsigned char数组(因为我们知道数组对象的成员是严格连续的,并且在使用sizeof()计算它们的大小时没有任何填充字节)。

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

因为我们知道unsigned char是以严格的二进制形式表示的,没有填充位,所以这里可以使用类型双关来查看成员data的二进制表示。
此工具可用于分析在特定实现中如何表示给定类型的值。
我看不到另一个安全和有用的应用类型双关语下的标准规范。

    • 5.关于铸件的评论...**

如果你想玩类型,最好定义你自己的转换函数,或者我们只使用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

相关问题