Tor*_*erg 11 c undefined-behavior unions language-lawyer type-punning
在使用-O1和-O2(包括gcc和clang)编译后,此代码打印不同的值:
#include <stdio.h>
static void check (int *h, long *k)
{
*h = 5;
*k = 6;
printf("%d\n", *h);
}
union MyU
{
long l;
int i;
};
int main (void)
{
union MyU u;
check(&u.i, &u.l);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
我认为它应该是未定义的行为,因为指针别名,但我无法确切地指出代码的哪一部分是被禁止的.
它写入一个union元素然后从另一个读取,但是根据允许的缺陷报告#283.通过指针而不是直接访问union元素时是UB吗?
这个问题类似于通过指针访问C联盟成员,但我认为其中一个从未得到完全回答.
我花了一段时间才意识到问题的症结在这里。 DR236对此进行了讨论。问题实际上是关于将指针传递给指向重叠存储的函数;以及是否允许编译器假设此类指针可以相互别名。
如果我们只是讨论工会成员的别名,那就更简单了。在下面的代码中:
u.i = 5;
u.l = 6;
printf("%d\n", u.i);
Run Code Online (Sandbox Code Playgroud)
该行为未定义,因为 的有效类型u是long; 即 的存储u包含存储为 的值long。但是通过类型的左值访问这些字节int违反了 6.5p7 的别名规则。关于具有未指定值的不活跃工会成员的文本不适用(IMO);别名规则胜过这一点,并且当不违反别名规则时(例如,通过字符类型的左值访问时),该文本就会发挥作用。
如果我们交换上面前两行的顺序,那么程序将是明确定义的。
然而,当访问“隐藏”在函数指针后面时,一切似乎都发生了变化。
DR236 通过两个示例解决了这个问题。这两个例子都check()与这篇文章中一样。示例 1malloc包含一些内存和通道h,并且k都指向该块的开头。示例 2 有一个类似于这篇文章的联合。
他们的结论是,例 1 是“未解决的”,例 2 是 UB。然而,这篇优秀的博客文章指出,DR236 在得出这些结论时使用的逻辑是不一致的。(感谢 Tor Klingberg 发现了这一点)。
DR236的最后一行还说:
两个程序都通过使用指针调用函数 f 来调用未定义的行为,
qi并且qd指针具有不同的类型但指定相同的存储区域。翻译者完全有权按照通常的别名规则重新安排*qi访问*qd。
(显然与先前声称示例 1 尚未解决的说法相矛盾)。
这句话表明编译器可以假设传递给函数的两个指针是否restrict具有不同的类型,但是我在标准中找不到任何这样的措辞,甚至无法解决编译器通过重新排序访问的问题指针。
有人建议,别名规则允许编译器得出 anint *和 along *无法访问同一内存的结论。然而,实施例1和实施例2与此完全矛盾。
如果指针具有相同的类型,那么我认为我们同意编译器无法重新排序访问,因为它们可能都指向同一个对象。restrict除非专门声明,否则编译器必须假设指针不是。
然而,我看不出这种情况与示例 1 和示例 2 的情况之间有什么区别。
DR236 还说:
普遍的理解是联合声明必须在翻译单元中可见。
这再次与示例 2 是 UB 的说法相矛盾,因为在示例 2 中,所有代码都在同一个翻译单元中。
我的结论:在我看来,C99 措辞表明编译器不应被允许重新排序*h = 5;,*k = 6;以防它们为重叠存储别名。尽管 DR236 与 C99 的措辞相矛盾并且没有澄清问题。但是之后阅读*h应该会导致未定义的行为,因此允许编译器生成5or6或其他任何内容的输出。
在我的阅读中,如果您修改check()为 那么*k = 6; *h=5;它应该被明确定义为 print 5。看看编译器在这种情况下是否仍然执行其他操作以及编译器执行此操作的基本原理会很有趣。