C语言 是否允许使用联合来进行类型双关,如果不允许,为什么?

hl0ma9xz  于 2023-10-16  发布在  其他
关注(0)|答案(5)|浏览(105)

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

j2cgzkjk

j2cgzkjk1#

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

628mspwn

628mspwn2#

联合最初的目的是当你想能够表示不同的类型时,节省保存空间,我们称之为variant typeBoost.Variant就是一个很好的例子。
另一个常见的用法是type punning,它的有效性还在争论中,但实际上大多数编译器都支持它,我们可以看到gcc文档中对它的支持:
阅读来自不同的联合成员而不是最近写入的成员的做法(称为“类型双关”)是很常见的。即使使用-fstrict-aliasing,只要通过union类型访问内存,也允许类型双关。所以,上面的代码按预期工作。
注意,它说 * 即使使用-fstrict-aliasing,也允许使用类型双关语 *,这表明存在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涵盖了很多相同的地面,我们可以看到如何灰色的领土可以。

bn31dyow

bn31dyow3#

有(或者至少在C90中)两个修改来实现这种未定义的行为。第一个是允许编译器生成额外的代码来跟踪联合体中的内容,并在访问错误的成员时生成信号。在实践中,我不认为任何人曾经做过(也许中线?)。另一个是优化的可能性,这开辟了,这些都是使用。我使用的编译器会将写入延迟到最后可能的时刻,理由是它可能没有必要(因为变量超出了范围,或者随后会写入不同的值)。从逻辑上讲,人们会期望当工会可见时,这种优化将被关闭,但在Microsoft C的最早版本中并没有。
类型双关的问题是复杂的。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的读取。(我认为这从来不是保证的意图。但目前的标准措辞确实保证了这一点。)
编辑:
由于其他答案认为,行为实际上是被定义的(主要是基于引用一个非规范性的注解,断章取义):
这里的正确答案是pablo 1977:当涉及类型双关时,该标准没有尝试定义行为。可能的原因是它没有可定义的可移植行为。这并不妨碍特定的实现定义它;虽然我不记得有任何关于这个问题的具体讨论,但我非常确定其意图是实现定义一些东西(如果不是全部的话,也是大多数)。
关于使用union进行类型双关:当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,即使Node是通过inner初始化的。(事实上,§6.5.2.3/6以“做出一项特殊保证.”开始,并明确允许这一点,这是一个非常强烈的迹象,表明所有其他情况都意味着是未定义的行为。当然,在§4/2中有这样的声明:“未定义的行为在本国际标准中以”未定义的行为“或 * 通过省略任何明确的行为定义 * 来表示”;为了证明行为不是未定义的,你必须说明它在标准中定义的位置。)

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

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

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

ss2ws0br

ss2ws0br4#

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

2ul0zpep

2ul0zpep5#

*简要回答: 类型双关**在某些情况下是安全的。另一方面,虽然这似乎是一个非常众所周知的做法,但标准似乎并不太感兴趣,使其正式。

我将只谈论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)。
  • 但我们可以确定一些细节:

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

3.类型双关与类型表示

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

4.安全案例:unsigned char

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

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

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

**5.关于演员的评论. *

如果你想使用类型,最好定义自己的转换函数,或者直接使用cast。我们可以记住这个简单的例子:

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

相关问题