当前位置: 首页 > 知识库问答 >
问题:

联合和类型双关语

霍伟彦
2023-03-14

我已经找了一段时间了,但是找不到一个明确的答案。

很多人说使用联合来键入双关语是不明确的,也是不好的做法。这是为什么呢?我看不出它会做任何未定义的事情的任何理由,考虑到你写入原始信息的内存不会自动改变(除非它超出了堆栈的范围,但这不是一个联合问题,这将是一个糟糕的设计)。

人们引用严格的混淆现象规则,但在我看来,这就像说你做不到,因为你做不到。

此外,如果不打双关语,工会还有什么意义?我在某个地方看到,它们应该被用来在不同的时间为不同的信息使用相同的存储位置,但是为什么不在再次使用之前删除这些信息呢?

总结一下:

  1. 为什么使用联合进行类型双关不好?
  2. 如果不是这样,他们有什么意义?

额外信息:我主要使用C语言,但我想了解这一点和C语言。具体来说,我使用联合在浮点和原始十六进制之间进行转换,以通过CAN总线发送。

共有3个答案

韦志新
2023-03-14

在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时,编译器假定piand 和读取 的顺序。(我认为这从来都不是保证的目的。但该标准目前的措辞确实保证了这一点。)

编辑:

因为其他答案认为行为实际上是被定义的(主要基于引用一个脱离上下文的非规范性注释):

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

关于使用类型双关的联合:当C委员会开发C90时(在20世纪80年代后期),有一个明确的意图,允许调试执行额外的检查(例如使用胖指针进行边界检查)。从当时的讨论中可以清楚地看出,其意图是调试实现可能会缓存关于联合中最后一个初始化值的信息,如果您试图访问任何其他内容,就会陷入陷阱。这在6.7.2.1/16:中有明确的表述“在任何时候,至多一个成员的值可以存储在一个联合对象中。”访问一个不存在的值是未定义的行为;这类似于访问一个未初始化的变量。(当时有一些关于访问同类型的不同成员是否合法的讨论。然而,我不知道最后的决议是什么;大约在1990年后,我搬到了加州

关于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,即使节点是通过内部初始化的。(§6.5.2.3/6以“作出一项特殊保证......”开头的事实并继续明确允许这是一个非常强烈的迹象,表明所有其他情况都是未定义的行为。当然,在§4/2中,有声明“未定义的行为在本标准中以其他方式通过'未定义行为''或省略任何明确的行为定义来表示”;为了论证该行为不是未定义的,您必须显示它在标准中定义的位置。

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

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

实际上相当便携。(当然在大型机上是不行的。)不起作用的是像我的第一个例子一样的东西,其中别名对于编译器是不可见的。(我很确定这是标准的缺陷。我似乎记得曾经看过这方面的医生。)

姚向晨
2023-03-14

Unions最初的目的是在您希望能够表示不同类型时节省空间,我们称之为变体类型,请参阅Boost。Variant就是一个很好的例子。

另一个常见的用法是类型双关。这种用法的有效性还存在争议,但实际上大多数编译器都支持它,我们可以看到gcc记录了它的支持:

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

请注意,它说即使使用-fstrict-别名,也允许使用类型双关语,这表明存在别名问题。

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

如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则值的对象表示的适当部分将被重新解释为6.2.6中描述的新类型中的对象表示(有时称为“类型双关”的过程)。这可能是陷阱表示。

在C11中,这将是脚注95

虽然在std-讨论邮件组主题通过联盟类型Pning中提出了参数,但这是未充分说明的,这似乎是合理的,因为DR 283没有添加新的规范性措辞,只是一个脚注:

在我看来,这是C中一个未明确规定的语义泥潭。对于哪些案例定义了行为,哪些案例没有定义行为,实现者和C委员会之间还没有达成共识[……]

在C中,不清楚是否定义了行为。

本次讨论还涵盖了至少一个原因,即为什么允许通过联合使用类型双关语是不可取的:

[...]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 ;

我们可以看到,使用godbolt确实会生成相同的代码,如果您的编译器没有生成相同的代码,就应该认为是一个错误:

如果您的实现是这样,我建议您对它进行bug归档。打破真正的优化(任何基于基于类型的别名分析)来解决某些特定编译器的性能问题,对我来说似乎是个坏主意。

博文类型双关、严格别名和优化也得出类似的结论。

未定义的行为邮件列表讨论:键入双关语以避免复制涵盖了很多相同的领域,我们可以看到领域是多么灰色。

鲁博赡
2023-03-14

重复一遍,通过联合进行类型双关在C中完全可以(但在C中不行)。相比之下,使用指针强制转换这样做违反了C99严格别名,并且是有问题的,因为不同的类型可能有不同的对齐要求,如果做错了,您可能会引发SIGBUS。有了工会,这从来都不是问题。

C标准的相关引述是:

C89第3.3.2.3§5节:

如果在值存储在对象的其他成员中之后访问联合对象的成员,则行为是由实现定义的

C11节6.5.2.3§3:

后缀表达式,后跟 .运算符和标识符指定结构或联合对象的成员。该值是指定成员的值

加上以下脚注95:

如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将重新解释为新类型中的对象表示形式,如 6.2.6 中所述(此html" target="_blank">过程有时称为“类型双关”)。这可能是一个陷阱表示。

这应该非常清楚。

James感到困惑,因为C11第6.7.2.1§16节内容如下

任何时候,联合对象中最多可以存储一个成员的值。

这看起来矛盾,但事实并非如此:与C相反,在C中,没有活动成员的概念,通过不兼容类型的表达式访问单个存储值是完全可以的。

另见C11附件J.1§1:

与上次存储到[中的联合成员以外的联合成员对应的字节值未指定]。

在C99中,这通常是

存储到[未指定]中的最后一个成员以外的联合成员的值

这是不正确的。由于附件不规范,因此它没有对自己的TC进行评级,必须等到下一次标准修订版才能确定。

标准C(和C90)的GNU扩展明确允许使用联合进行类型双关。其他不支持GNU扩展的编译器也可能支持联合类型双关语,但它不是基础语言标准的一部分。

 类似资料:
  • 使用“关联类型”可以增强代码的可读性,其方式是移动内部类型到一个 trait 作为output(输出)类型。这个 trait 的定义的语法如下: // `A` 和 `B` 在 trait 里面通过`type` 关键字来定义。 // (注意:此处的 `type` 不同于用作别名时的 `type`)。 trait Contains { type A; type B; // 通常

  • 关联类型 定义一个协议时, 有时在协议定义里声明一个或多个关联类型是很有用的. 关联类型给协议中用到的类型一个占位符名称. 直到采纳协议时, 才指定用于该关联类型的实际类型. 关联类型通过associatedtype关键字指定. 关联类型的应用 protocol Container { associatedtype ItemType mutating func append(_ i

  • null 在编写代码时,什么时候应该选择关联类型而不是泛型类型参数,什么时候应该做相反的操作?

  • 主要内容:TypeScript,JavaScript,TypeScript,JavaScript,联合类型数组,TypeScript,JavaScript联合类型(Union Types)可以通过管道(|)将变量设置多种类型,赋值时可以根据设置的类型来赋值。 注意:只能赋值指定的类型,如果赋值其它类型就会报错。 创建联合类型的语法格式如下: 实例 声明一个联合类型: TypeScript var val:string|number val = 12 console.log("数字为 "+ val

  • 本节介绍联合类型,它使用管道符 | 把多个类型连起来,表示它可能是这些类型中的其中一个。我们把 | 理解成 or,这样便于轻松记忆。 1. 慕课解释 联合类型与交叉类型很有关联,但是使用上却完全不同。区别在于:联合类型表示取值为多种中的一种类型,而交叉类型每次都是多个类型的合并类型。 语法为:类型一 | 类型二。 2. 简单示例 联合类型之间使用竖线 “|” 分隔: let currentMont

  • 关联类型是 Rust 类型系统中非常强大的一部分。它涉及到‘类型族’的概念,换句话说,就是把多种类型归于一类。这个描述可能比较抽象,所以让我们深入研究一个例子。如果你想编写一个Graph trait,你需要泛型化两个类型:点类型和边类型。所以你可能会像这样写一个 trait,Graph<N, E>: trait Graph<N, E> { fn has_edge(&self, &N, &N