鉴于代码:
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
上使用address-of运算符,或者在该成员处于活动状态时使用结果指针,同样使用q.v2
。由于p1
,p2
和p3
属于同一类型,因此使用p3->v1
访问p1->v1
和{{1}应该完全合法}访问p3->v2
。
我没有看到任何可以证明编译器无法输出1234的原因,但是许多编译器(包括clang和gcc)生成的代码输出4321.我认为发生的事情是他们认为p3上的操作实际上不会更改内存中任何位的内容,它们可以完全被忽略,但我没有看到标准中有任何理由忽略p2->v2
用于将数据从p3
复制到p1->v1
的事实p2->v2
,反之亦然。
标准中是否有任何可以证明这种行为的理由,或者编制者是否只是不遵循它?
答案 0 :(得分:10)
我相信你的代码是符合的,并且GCC和Clang的-fstrict-aliasing
模式存在缺陷。
我找不到C标准的正确部分,但是在我为C ++模式编译代码时会出现同样的问题,我确实找到了C ++标准的相关段落。
在C ++标准中,[class.union] / 5定义了在union访问表达式上使用operator =
时会发生什么。 C ++标准规定当联合涉及内置运算符=
的成员访问表达式时,联合的活动成员将更改为表达式中涉及的成员(如果类型具有一个简单的构造函数) ,但因为这是C代码,它确实有一个简单的构造函数)。
请注意,write_s2x
无法更改联合的活动成员,因为赋值表达式中不包含联合。你的代码并不认为会发生这种情况,所以没关系。
即使我使用placement new
显式更改哪个union成员处于活动状态,这应该是活动成员更改的编译器的提示,GCC仍会生成输出4321
的代码。 / p>
这看起来像是GCC和Clang的一个错误,假设活动联盟成员的切换不能在这里发生,因为他们无法识别p1
,p2
和p3
的可能性指向同一个对象。
GCC和Clang(以及几乎所有其他编译器)都支持C / C ++的扩展,您可以在其中读取union的非活动成员(获取任何可能的垃圾值),但前提是您执行此访问涉及union的成员访问表达式。 如果 v1
不是活动成员,则read_s1x
将不会在此特定于实现的规则下定义行为,因为联合不在成员访问表达式中。但由于v1
是活跃成员,因此无关紧要。
这是一个复杂的案例,我希望我的分析是正确的,不是编译维护者或其中一个委员会的成员。
答案 1 :(得分:4)
对标准的严格解释,此代码可能不符合。让我们关注众所周知的§6.5p7的文本:
对象的存储值只能由具有其中一个的左值表达式访问 以下类型:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的类型的限定版本,
- 对应于有效类型的有符号或无符号类型的类型 对象,
- 对应于合格版本的有符号或无符号类型的类型 有效的对象类型,
- 一种聚合或联合类型,其中包含上述类型之一 成员(包括递归地,子集合或包含的联合的成员)或
- 字符类型。
(强调我的)
您的功能read_s1x()
和write_s2x()
执行我在整个代码的上下文中在上方标记为粗体的相反。只有这一段,你可以断定它是不允许的:指向union s1s2
的指针将被允许别名指向struct s1
的指针,但反之亦然。
如果您在test()
中手动“内联”这些功能,那么这种解释当然意味着代码必须按预期工作。对于i686-w64-mingw32
,gcc 6.2确实就是这种情况。
添加两个论据,支持上述严格解释:
虽然总是允许使用char *
对任何指针进行别名,但字符数组不能被任何其他类型别名化。
考虑(此处不相关)§6.5.2.3p6:
为了简化工会的使用,我们提出了一项特殊保证:如果工会包含 几个结构共享一个共同的初始序列(见下文),如果是联盟 对象当前包含这些结构中的一个,允许检查公共结构 其中任何一个的初始部分声明已完成的工会类型的任何地方 是可见的。
(再次强调我的) - 典型的解释是可见直接意味着在有关功能的范围内,而不是“在翻译单元的某个地方”...所以这个保证没有不包含一个函数,该函数指向struct
成员的union
之一。
答案 2 :(得分:0)
我没有阅读标准,但在严格别名模式下使用指针(即使用-fstrict-alising
)是危险的。请参阅gcc online doc:
特别注意这样的代码:
union a_union {
int i;
double d;
};
int f() {
union a_union t;
t.d = 3.0;
return t.i;
}
从不同的工会成员阅读的做法比最近写的(称为
type-punning
)很常见。即使使用-fstrict-aliasing
,也允许使用类型 - 双关语,前提是通过联合类型访问内存。因此,上面的代码按预期工作。请参阅结构联合枚举和位字段实现。但是,此代码可能不会:
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;
}
{ - 1}}选项在级别-O2,-O3,-Os。
处启用
在第二个例子中发现了类似的东西吗?
答案 3 :(得分:-2)
它不是符合或不符合 - 它是优化“陷阱”之一。您的所有数据结构都已经过优化,并且您将相同的指针传递给优化的输出数据,因此执行树将简化为值的简单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
这是一个非常简单的测试,只是人为地制造了一点点复杂。