c-C11标准中工会示例的说明

  • 本文关键字:说明 标准 c-C11 c unions c11
  • 更新时间 :
  • 英文 :


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

以下不是有效的片段(因为联合类型不是在函数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段(增加了重点):

为了简化工会的使用,提供了一项特殊保证:如果并集包含多个结构,这些结构共享一个共同的首字母序列(见下文),并且如果联合对象当前包含一个在这些结构中,允许检查常见的初始其中任何一个的一部分完成类型的声明所在的任何位置的可见。两个结构共享一个公共首字母sequence如果相应的成员具有兼容的类型(并且,对于具有相同宽度的比特字段)成员。

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

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


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

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

请注意,正如本文所讨论的,ISO/IEC文件中标题为注释示例的部分是"非规范性的",这意味着它们实际上并不构成规范的一部分。


有人认为此代码违反了严格的混叠规则。这是C11 6.5/7:的规则

对象的存储值只能由具有以下类型之一的左值表达式访问:

  • 与对象的有效类型兼容的类型,[…]

在该示例中,正在访问的对象(由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只允许通过类型为字符类型的lvalue或包含类型为union Ustruct S的对象的其他结构或并集访问这些类型的对象,因此它不会对试图使用任何这些lvalue的代码的行为提出任何要求。

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

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

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*,用于标识其类型的对象,或union s1s2的任一成员的新派生的地址时。在任何情况下,实现都有足够的理由关心对原始和派生lvalue的操作是否会相互影响,它将能够看到它们之间的关系。

然而,请注意,不需要这样的实现来允许代码中出现混叠的可能性,如下所示:

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[]的元素的更可疑的可能性。

许多寻求以有用的方式有效地支持公共初始序列规则的实现将能够处理第一个,即使没有声明CCD_,我不知道标准的作者是否打算仅仅添加一个union类型声明就迫使编译器考虑其中成员的公共初始序列之间的任意混叠的可能性。提到并集类型,我能看到的最自然的意图是,无法感知第一个例子中出现的众多线索中的任何一个的编译器可以使用以两个类型为特征的任何完整并集类型声明的存在或不存在来指示一个此类类型的lvalue是否可以用于访问另一个类型。

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

最新更新