澄清 C11 标准中的联合示例

Clarification on an example of unions in C11 standard

C11标准6.5.2.3中给出了以下示例

The following is not a valid fragment (because the union type is not visible within function f):

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 *p1, struct t2 *p2)
{
   if (p1->m < 0)
   p2->m = -p2->m;
   return p1->m;
}
int g()
{
   union {
      struct t1 s1;
      struct t2 s2;
   } u;
   /* ... */
   return f(&u.s1, &u.s2);
}

为什么联合类型对函数 f 可见很重要?

在阅读相关部分几遍时,我在包含部分中没有看到任何不允许这样做的内容。

这很重要,因为 6.5.2.3 第 6 段(强调已添加):

One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

这不是需要诊断的错误(语法错误或违反约束),但行为未定义,因为 struct t1struct t2 对象的 m 成员占用相同的存储,但是因为 struct t1struct t2 是不同的类型,允许编译器假设它们不是——特别是对 p1->m 的更改不会影响 p2->m。例如,编译器可以在第一次访问时将 p1->m 的值保存在寄存器中,然后在第二次访问时不从内存中重新加载它。

注意:这个答案没有直接回答你的问题,但我认为它是相关的,而且太大了,无法发表评论。


我认为代码中的示例实际上是正确的。联合公共初始序列规则确实不适用;但也没有任何其他规则会使此代码不正确。

公共初始序列规则的目的是保证结构的相同布局。然而,这在这里甚至不是问题,因为结构只包含一个 int,并且结构不允许有初始填充。

请注意,正如 here 所讨论的,ISO/IEC 文档中标题为 NoteExample 的部分是 "non-normative" 这意味着它们实际上并不构成规范的一部分。


有人建议此代码违反了严格的别名规则。这是来自 C11 6.5/7 的规则:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object, [...]

在示例中,正在访问的 object(表示为 p2->mp1->m)的类型为 int。左值表达式 p1->mp2->m 的类型为 int。由于intint兼容,所以没有违规。

确实 p2->m 表示 (*p2).m,但是此表达式不会访问 *p2。它只访问 m.


以下任一项未定义:

*p1 = *(struct t1 *)p2;   // strict aliasing: struct t2 not compatible with struct t1
p2->m = p1->m++;          // object modified twice without sequence point

鉴于声明:

union U { int x; } u,*up = &u;
struct S { int x; } s,*sp = &s;

左值 u.xup->xs.xsp->x 都是 int 类型,但是对任何这些左值的任何访问都会(至少使用如图所示初始化的指针)还将访问类型为 union Ustruct S 的对象的存储值。由于 N1570 6.5p7 仅允许通过类型为字符类型的左值或 包含 类型 union U 和 [= 的对象的其他结构或联合访问这些类型的对象19=],它不会对尝试使用任何这些左值的代码的行为强加任何要求。

我认为很明显,标准的作者希望编译器至少在某些情况下允许使用成员类型的左值访问结构或联合类型的对象,但不一定允许成员类型的任意左值访问结构或联合类型的对象。没有任何规范来区分应允许或不允许此类访问的情况,但有一个脚注表明该规则的目的是表明何时可以或不可以使用别名。

如果将规则解释为仅适用于左值以别名的方式使用其他类型的看似无关的左值的情况,则这样的解释将定义如下代码的行为:

struct s1 {int x; float y;};
struct s2 {int x; double y;};
union s1s2 { struct s1 v1; struct s2 v2; };

int get_x(void *p) { return ((struct s1*)p)->x; }

当后者传递一个 struct s1*struct s2*union s1s2* 来标识其类型的对象时,或者 freshly-derived union s1s2 任一成员的地址。在任何上下文中,如果实现能够看到足够的理由来关心对原始左值和派生左值的操作是否会相互影响,那么它就能够看到它们之间的关系。

但是请注意,这样的实现不需要允许在如下代码中使用别名的可能性:

struct position {double px,py,pz;};
struct velocity {double vx,vy,vz;};

void update_vectors(struct position *pos, struct velocity *vel, int n)
{
  for (int i=0; i<n; i++)
  {
    pos[i].px += vel[i].vx;
    pos[i].py += vel[i].vy;
    pos[i].pz += vel[i].vz;
  }
}

尽管公共初始序列保证似乎允许这样做。

这两个示例之间有很多差异,因此有许多迹象表明编译器可以使用以允许第一个代码传递 struct s2* 的现实可能性,它可能会访问 struct s2, 而不必考虑更可疑的可能性,即在第二次检查中对 pos[] 的操作可能会影响 vel[].

的元素

许多寻求以有用的方式有效支持公共初始序列规则的实现即使没有声明 union 类型也能够处理第一个,我不知道标准的作者旨在仅添加 union 类型声明应该强制编译器允许在其中的成员的公共初始序列之间任意别名的可能性。我能看到的提及联合类型的最自然的意图是,无法感知第一个示例中出现的众多线索中的任何一个的编译器可以使用具有两种类型的任何完整联合类型声明的存在与否来指示是否存在一种此类类型的左值可用于访问另一种类型。

请注意,N1570 P6.5p7 及其前身都没有努力描述所有情况,在这些情况下,在给定使用聚合的代码时,质量实现应该表现得可预测。大多数此类案例都作为实施质量问题留了下来。由于几乎任何他们认为合适的原因都允许低质量但符合标准的实现表现出荒谬的行为,因此没有必要使标准复杂化,因为任何真正努力编写高质量实现的人都会处理是否它是一致性所必需的。