[英]Why is it invalid for a union type declared in one function to be used in another function?
当我阅读ISO / IEC 9899:1999(见:6.5.2.3)时,我看到了一个这样的例子(强调我的):
以下不是有效的片段 (因为联合类型在函数
f
不可见 ):struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 * p1, struct t2 * p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); }
我测试时发现没有错误和警告。
我的问题是:为什么这个片段无效?
该示例试图预先说明段落1 (强调我的):
6.5.2.3¶6
为了简化联合的使用,我们做了一个特殊的保证:如果一个联合包含几个共享一个共同初始序列的结构(见下文),并且如果联合对象当前包含这些结构中的一个, 则允许检查公共其中任何一个的初始部分都可以看到完整类型的联合声明 。 如果对应的成员具有一个或多个初始成员的序列的兼容类型(并且对于位字段,具有相同的宽度),则两个结构共享共同的初始序列。
由于f
在g
之前声明,而且未命名的union类型对于g
是局部的,因此毫无疑问在f
不到联合类型。
该示例未显示如何初始化u
,但假设最后一次写入成员是u.s2.m
,则该函数具有未定义的行为,因为它检查p1->m
而没有共同的初始序列保证生效。
另一方面,如果它是函数调用之前最后写入的u.s1.m
,则访问p2->m
是未定义的行为。
请注意, f
本身并非无效。 这是一个非常合理的函数定义。 未定义的行为源于传入它&u.s1
和&u.s2
作为参数。 这就是导致未定义行为的原因。
1 - 我引用了N1170 ,C11标准草案。 但规范应该是相同的,只需要向上/向下移动一个或两个段落。
下面是严格的别名规则:C(或C ++)编译器做出的一个假设是,取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即别名相互作用)。
这个功能
int f(struct t1* p1, struct t2* p2);
假设p1 != p2
因为它们正式指向不同的类型。 结果,优化器可以假设p2->m = -p2->m;
对p1->m
没有影响; 它可以先将p1->m
的值读入寄存器,将其与0比较,如果比较小于0,则执行p2->m = -p2->m;
最后返回寄存器值不变!
这里的联合是在二进制级别上使p1 == p2
的唯一方法,因为所有联合成员都具有相同的地址。
另一个例子:
struct t1 { int m; };
struct t2 { int m; };
int f(struct t1* p1, struct t2* p2)
{
if (p1->m < 0) p2->m = -p2->m;
return p1->m;
}
int g()
{
union {
struct t1 s1;
struct t2 s2;
} u;
u.s1.m = -1;
return f(&u.s1, &u.s2);
}
什么是必须g
返回? 根据常识+1
(我们在f
中将-1改为+1)。 但是如果我们用-O1
优化来看gcc的生成程序集
f:
cmp DWORD PTR [rdi], 0
js .L3
.L2:
mov eax, DWORD PTR [rdi]
ret
.L3:
neg DWORD PTR [rsi]
jmp .L2
g:
mov eax, 1
ret
到目前为止,所有都是例外。 但是当我们尝试使用-O2
f:
mov eax, DWORD PTR [rdi]
test eax, eax
js .L4
ret
.L4:
neg DWORD PTR [rsi]
ret
g:
mov eax, -1
ret
返回值现在是硬编码的-1
这是因为开头的f
在eax
寄存器( mov eax, DWORD PTR [rdi]
)中缓存了p1->m
的值, 并且在p2->m = -p2->m;
之后不重读它 p2->m = -p2->m;
( neg DWORD PTR [rsi]
) - 它返回eax
不变。
union此处仅用于union对象的所有非静态数据成员具有相同的地址。 结果&u.s1 == &u.s2
。
有人不懂汇编代码,可以在c / c ++中显示如何严格别名影响f代码:
int f(struct t1* p1, struct t2* p2)
{
int a = p1->m;
if (a < 0) p2->m = -p2->m;
return a;
}
编译器缓存p1->m
值在本地var a
(当然实际上在寄存器中)并返回它,尽管p2->m = -p2->m;
改变p1->m
。 但编译器假设p1
内存不受影响,因为它假设p2
指向另一个不与p1
重叠的内存
因此,对于不同的编译器和不同的优化级别,相同的源代码可以返回不同的值(-1或+1)。 这样和未定义的行为
通用初始序列规则的主要目的之一是允许函数可互换地在许多相似结构上操作。 要求编译器假定任何作用于结构的函数可能会改变共享共同初始序列的任何其他结构中的相应成员,但是会削弱有用的优化。
尽管大多数依赖于公共初始序列保证的代码都使用了一些易于识别的模式,例如
struct genericFoo {int size; short mode; };
struct fancyFoo {int size; short mode, biz, boz, baz; };
struct bigFoo {int size; short mode; char payload[5000]; };
union anyKindOfFoo {struct genericFoo genericFoo;
struct fancyFoo fancyFoo;
struct bigFoo bigFoo;};
...
if (readSharedMemberOfGenericFoo( myUnion->genericFoo ))
accessThingAsFancyFoo( myUnion->fancyFoo );
return readSharedMemberOfGenericFoo( myUnion->genericFoo );
重新审视对作用于不同联盟成员的函数的调用之间的联合,标准的作者指出,被调用函数中联合类型的可见性应该是函数是否应该识别访问例如字段mode
的可能性的决定因素。一个FancyFoo
可能会影响场mode
一的genericFoo
。 要求拥有一个包含所有类型结构的联合的要求,这些结构的地址可能在与该函数相同的编译单元中传递给readSharedMemberOfGeneric
,这使得公共初始序列规则没有其他方面有用,但至少会允许某些模式,如以上可用。
gcc和clang的作者认为将联合声明视为表明所涉及的类型可能涉及上述结构的指示将是对优化的不切实际的障碍,并且认为由于标准不要求它们支持这样的通过其他方式构建,他们根本就不支持它们。 因此,需要以任何有意义的方式利用公共初始序列保证的代码的实际要求不是确保联合类型声明是可见的,而是确保使用-fno-strict-aliasing
调用clang和gcc旗。 还包括一个可见的联合声明,如果实际不会受到伤害,但它既不必要也不足以确保gcc和clang的正确行为。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.