C 这种使用工会的做法是否严格符合规定?

C 这种使用工会的做法是否严格符合规定?,c,gcc,clang,strict-aliasing,C,Gcc,Clang,Strict Aliasing,鉴于代码: struct s1 {unsigned short x;}; struct s2 {unsigned short x;}; union s1s2 { struct s1 v1; struct s2 v2; }; static int read_s1x(struct s1 *p) { return p->x; } static void write_s2x(struct s2 *p, int v) { p->x=v;} int test(union s1s2 *p1,

鉴于代码:

struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };

static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}

int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
  if (read_s1x(&p1->v1))
  {
    unsigned short temp;
    temp = p3->v1.x;
    p3->v2.x = temp;
    write_s2x(&p2->v2,1234);
    temp = p3->v2.x;
    p3->v1.x = temp;
  }
  return read_s1x(&p1->v1);
}
int test2(int x)
{
  union s1s2 q[2];
  q->v1.x = 4321;
  return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
  printf("%d\n",test2(0));
}
structs1{无符号短x;};
结构s2{无符号短x;};
联合s1s2{struct s1 v1;struct s2 v2;};
静态int read_s1x(结构s1*p){return p->x;}
静态void write_s2x(结构s2*p,int v){p->x=v;}
内部测试(接头s1s2*p1、接头s1s2*p2、接头s1s2*p3)
{
如果(读取s1x(&p1->v1))
{
无符号短时间;
温度=p3->v1.x;
p3->v2.x=温度;
写入_s2x(&p2->v21234);
温度=p3->v2.x;
p3->v1.x=温度;
}
返回read_s1x(&p1->v1);
}
inttest2(intx)
{
联合s1s2 q[2];
q->v1.x=4321;
回归测试(q,q+x,q+x);
}
#包括
内部主(空)
{
printf(“%d\n”,test2(0));
}
整个程序中存在一个union对象--
q
。其活动成员设置为
v1
,然后设置为
v2
,然后再次设置为
v1
。当该成员处于活动状态时,代码仅使用q.v1上的运算符地址或结果指针,同样地q.v2。由于
p1
p2
p3
都是相同的类型,使用
p3->v1
访问
p1->v1
,使用
p3->v2
访问
p2->v2
应该是完全合法的

我看不到任何东西可以证明编译器无法输出1234,但许多编译器(包括clang和gcc)生成的代码输出4321。我认为发生的事情是,他们决定p3上的操作实际上不会改变内存中任何位的内容,它们可以完全忽略,但是我在标准中没有看到任何东西可以证明忽略这样一个事实,即
p3
用于将数据从
p1->v1
复制到
p2->v2
,反之亦然


标准中是否有任何东西可以证明这种行为是正确的,或者编译器只是没有遵循它?

我相信您的代码是一致的,并且GCC和Clang的
-fstrict别名模式存在缺陷

<>我找不到C标准的正确部分,但是在C++中编译代码时遇到同样的问题,我确实找到了C++标准的相关段落。 <>在C++标准中,[Cal.Cusi]/5定义了在GoeAccess表达式上使用运算符<代码>=/COD>时会发生什么。C++标准声明,当一个联合参与了内置运算符<代码>=< /> >的成员访问表达式时,该联盟的活动成员被更改为表达式中涉及的成员(如果类型具有一个微不足道的构造函数,但因为这是C代码,则它有一个微不足道的构造函数)。 请注意,
write_s2x
无法更改联合的活动成员,因为赋值表达式中不涉及联合。您的代码不假设会发生这种情况,所以这是正常的

即使我使用placement
new
来显式更改哪个联合成员处于活动状态,这应该是对编译器的一个提示,即活动成员已更改,GCC仍然会生成输出
4321
的代码

这看起来像是GCC和Clang的一个错误,假设活动联合成员的切换不能在这里发生,因为它们无法识别
p1
p2
p3
都指向同一对象的可能性

GCC和Clang(以及几乎所有其他编译器)都支持对C/C++的扩展,在该扩展中,您可以读取联合的非活动成员(得到任何可能的垃圾值),但前提是您在涉及联合的成员访问表达式中执行此访问。如果
v1
不是活动成员,
read\u s1x
将不会被定义为此实现特定规则下的行为,因为联合不在成员访问表达式中。但是因为
v1
是活动成员,所以这不重要


这是一个复杂的案例,我希望我的分析是正确的,因为我不是编译器维护人员或委员会成员。

这不是一致性或不一致性的问题,而是优化“陷阱”之一。您的所有数据结构都已优化,并且您将相同的指针传递给优化后的数据,因此执行树中的值将简化为值的简单printf

  sub rsp, 8
  mov esi, 4321
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret
要改变它,你需要使这个“转移”功能易于产生副作用,并强制执行真正的任务。它将强制优化器不减少执行树中的节点:

int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/

main:
  sub rsp, 8
  mov esi, 1234
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret
这是一个相当琐碎的测试,只是人为地使它变得更复杂了一点

如果对本标准进行严格解释,则本规范可能不符合要求。让我们关注著名的§6.5p7的文本:

对象的存储值只能由左值表达式访问,该左值表达式具有 以下类型:
-与对象的有效类型兼容的类型,
-与对象的有效类型兼容的类型的限定版本,
-一种类型,它是与数据的有效类型相对应的有符号或无符号类型 对象,
-一种类型,它是与的限定版本相对应的有符号或无符号类型 对象的有效类型,
-一种聚合或联合类型,其中包括上述类型之一 成员(递归地包括子集合或包含的联合的成员),或
-字符类型

(强调矿山)

您的函数
read_s1x()
write_s2x()
在整个代码的上下文中执行与我上面标记为粗体的相反的。仅通过这一段,您就可以得出这样的结论:指向
union s1s2
的指针可以将指向
struct s1
的指针别名,但反之亦然

当然,这种解释意味着,如果您“内联”这些函数,代码必须按预期工作
union a_union {
  int i;
  double d;
};

int f() {
  union a_union t;
  t.d = 3.0;
  return t.i;
}
int f() {
   union a_union t;
   int* ip;
   t.d = 3.0;
   ip = &t.i;
   return *ip;
}
int f() {
  double d = 3.0;
  return ((union a_union *) &d)->i;
}