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