【发布时间】:2018-02-22 15:35:02
【问题描述】:
给定代码:
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));
}
整个程序中存在一个联合对象--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 和反之亦然。
标准中是否有任何内容可以证明这种行为是合理的,或者编译器根本不遵循它?
【问题讨论】:
-
如果代码是
unsigned x而不是unsigned short x,你看到同样的问题吗? -
@chux:是的。早期版本的代码还测试了将对象的字节复制到
unsigned char类型的两个变量,然后将它们写回(编译器也不支持),使用两个字节比四个字节更方便。问题是编译器完全优化了p3上的操作并丢失了由此提供的别名相关信息。 -
我怀疑
unsigned会以与unsigned short类似的方式失败。使用unsigned,我们可以搁置任何通常的促销问题 - 这不应该影响这一点。 -
@chux:虽然
unsigned short可以升级为int或unsigned,但将32767u 及以下值强制转换为int完全由标准在所有实现中定义。 -
@curiousguy:我刚刚发布了另一个令人讨厌的问题,它涉及内存写入的重新排序,而不是读取与写入的排序。我发现后一个特别好奇,因为它不涉及编译器优化读取和写入,该读取和写入本应强制其他一些读取和写入的顺序,但经过调整,以便将有条件写入变成无条件写入使用有条件选择的值写入。
标签: c gcc clang strict-aliasing