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
无法更改联合的活动成员,因为赋值表达式中不涉及联合。您的代码不假设会发生这种情况,所以这是正常的
即使我使用placementnew
来显式更改哪个联合成员处于活动状态,这应该是对编译器的一个提示,即活动成员已更改,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;
}