为啥在一个函数中声明的联合类型在另一个函数中使用无效?

Posted

技术标签:

【中文标题】为啥在一个函数中声明的联合类型在另一个函数中使用无效?【英文标题】:Why is it invalid for a union type declared in one function to be used in another function?为什么在一个函数中声明的联合类型在另一个函数中使用无效? 【发布时间】:2019-03-01 21:17:57 【问题描述】:

当我阅读 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);

我在测试时没有发现任何错误和警告。

我的问题是:为什么这个片段无效?

【问题讨论】:

f 可以假设 p1 != p2 因为它们指向不同的类型。并进行优化 - 读取寄存器中的p1-&gt;m 值并返回此寄存器。它假定p2-&gt;m = -p2-&gt;m 不修改 p1-&gt;m 出了什么问题。 union 这里的唯一方法是使 p1==p2 我冒昧地将图像转录成文字,希望我没有打错字。原件在编辑历史中可见:***.com/revisions/52511896/1 请注意,有许多无效程序可以干净地编译。事实上,C 标准中有这么多关于什么是有效、无效、UB、...的文本的原因是因为你不能依赖编译器来简单地检测和拒绝它们 简单修复...在第一个函数之前定义联合 【参考方案1】:

该示例试图预先说明该段落1(强调我的):

6.5.2.3 ¶6

为了简化联合的使用,我们做出了一项特殊保证: 如果一个并集包含多个共享一个共同首字母的结构 序列(见下文),如果联合对象当前包含一个 在这些结构中,允许检查共同的首字母 它们中的任何一个的一部分,在完成类型的声明的任何地方 工会的可见。两个结构共享一个共同的首字母 如果相应的成员具有兼容的类型(并且,对于 位域,相同的宽度)用于一个或多个初始的序列 成员。

由于fg 之前声明,而且未命名的联合类型是g 的本地类型,因此毫无疑问联合类型在f 中不可见。

该示例没有显示u 是如何初始化的,但假设最后写入成员是u.s2.m,该函数具有未定义的行为,因为它检查p1-&gt;m 而没有有效的通用初始序列保证。

如果在函数调用之前最后写入的是u.s1.m,那么反过来也是如此,那么访问p2-&gt;m 是未定义的行为。

注意f 本身并不是无效的。这是一个完全合理的函数定义。未定义的行为源于将 &amp;u.s1&amp;u.s2 作为参数传递给它。这就是导致未定义行为的原因。


1 - 我引用的是 C11 标准草案 n1570。但是规范应该是相同的,只需要向上/向下移动一两个段落。

【讨论】:

那么将f 更改为int* 并传入&amp;u.s1.mu.s2.m 会使其有效吗?因为那时是g 进行结构访问。 @Zastai - 你知道,我不确定。我认为这是一个值得独立思考的好问题。使用language-lawyer 标签发布它。应该很有趣。 @hacks - 是的。这就是为什么 GCC 和 Clang 在严格的别名下使用这个函数。他们没有理由假设这两个不相关的类型可能有别名。因为通常他们不能。从技术上讲,这一切都在“有效类型”条款集下处理。但是here's another example。这里编译器知道别名是可能的,所以它不会太激进。 @hacks - §6.5 ¶7。这些都是给同一个对象起别名的所有有效方法。因此,在原始示例中,p1p2 可能被假定为不是同一个对象的别名(即使它们通过联合以有效的方式进行)。我在前面的评论中给你的代码示例必须是保守的,因为联合包含结构作为成员,所以事情可能是别名。我认为公共子序列保证是这样表述的,以配合有效的类型要求。 @StoryTeller;非常感谢您的解释和耐心:)【参考方案2】:

下面是严格的别名规则:C(或 C++)编译器做出的一个假设是,取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即相互别名。)

这个功能

int f(struct t1* p1, struct t2* p2);

假设 p1 != p2 因为它们正式指向不同的类型。因此,优化者可能会假设p2-&gt;m = -p2-&gt;m;p1-&gt;m 没有影响;它可以先将p1-&gt;m的值读入一个寄存器,与0比较,如果小于0,则做p2-&gt;m = -p2-&gt;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)。但是如果我们看一下 gcc 的 generate 程序集加上-O1 优化

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

这是因为一开始fp1-&gt;m的值缓存在eax寄存器(mov eax, DWORD PTR [rdi])中,而在p2-&gt;m = -p2-&gt;m;(neg DWORD PTR [rsi])之后不再重读 ) - 它返回 eax 不变。


union 这里只用于 联合对象的所有非静态数据成员都具有相同的地址。 结果&amp;u.s1 == &amp;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-&gt;m 本地变量a 中的值(当然实际上在寄存器中)并返回它,尽管p2-&gt;m = -p2-&gt;m; 更改p1-&gt;m。但编译器假定p1 内存不受影响,因为它假定p2 指向另一个不与p1 重叠的内存

因此,使用不同的编译器和不同的优化级别,相同的源代码可以返回不同的值(-1 或 +1)。所以和未定义的行为原样

【讨论】:

@StoryTeller - 是的,same (-1) 和 clang。但是 msvc 和 icc 总是返回 +1(这里没有严格的别名) 作为附录,值得一提的是,将 f() 的参数声明为 volatile 将阻止这种优化。从技术上讲,根据标准,这仍然是未定义的行为,但大多数编译器应该给出一个理智的答案。但是,为什么有人会想编写如此奇怪的代码,这超出了我的理解。 :-/ 该标准的目的是确保即使是过于原始的编译器也无法在调用站点识别对 someFunction(&amp;myUnion-&gt;member1) 的调用可能与调用者使用的联合中的其他成员交互,可以在被调用的函数中识别出可能会发生这种交互。在实践中,gcc 和 clang 在任何情况下都不能可靠地识别这种交互,即使完整的联合声明是可见的,并且当编译器能够看到共享公共初始序列的结构实际上是 相同的联合对象数组. @supercat - 在我看来,这里的工会根本不相关。基于指向 2 个不同类型的指针不能重叠的假设,这发挥了编译器优化(在 f 中)的作用。所以p2指针修改内存不会影响p1的内存。此处的联合仅用于具有不同类型但指向相同内存的格式 2 指针。因此,虽然我们可以访问联合的非活动成员(如果它与活动“相似”),但一次使用指向 2 个联合成员的指针,即使它具有“公共初始序列”(甚至完全相等)导致这个 ub @supercat 我认为更快的编译器必须假定 2 个指向具有“共同初始序列”可以的 2 种类型的指针指向相同的内存。 t1t2 就是这样的类型。因此(在我看来)编译器假设这样的指针可以别名是合乎逻辑的。但现在 gcc/clang 不这样做。我也很认真地重新设计(扩展)术语什么是“通用初始序列”【参考方案3】:

通用初始序列规则的主要目的之一是允许函数可互换地对许多相似的结构进行操作。但是,要求编译器假定任何作用于结构的函数都可能更改任何其他共享公共初始序列的结构中的相应成员,这会损害有用的优化。

尽管大多数依赖于通用初始序列保证的代码都使用了一些易于识别的模式,例如

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 );

重新审视对作用于不同联合成员的函数的调用之间的联合,该标准的作者指出,被调用函数中联合类型的可见性应该是函数是否应该识别访问例如的可能性的决定因素。 FancyFoo 的字段 mode 可能会影响 genericFoo 的字段 mode。要求有一个包含所有类型结构的联合,这些结构的地址可能与该函数在同一编译单元中传递给readSharedMemberOfGeneric,这使得通用初始序列规则的用处不如其他规则,但至少允许某些模式像上面一样可用。

gcc 和 clang 的作者认为,将联合声明视为所涉及的类型可能涉及上述构造的指示对于优化来说是一个不切实际的障碍,并认为由于标准不需要它们要通过其他方式支持此类构造,他们根本不会支持它们。因此,需要以任何有意义的方式利用通用初始序列保证的代码的真正要求不是确保联合类型声明是可见的,而是确保使用-fno-strict-aliasing 标志调用clang 和gcc。在实际情况下还包括一个可见的联合声明不会受到伤害,但它既没有必要也不足以确保 gcc 和 clang 的正确行为。

【讨论】:

你试着说如果我们有S1* p1;S2* p2;S1S2有共同的初始序列并且存在可见声明union U S1 s1; S2 s2; ;编译器不能假设p1 != p2,但是如果删除 U 声明 - 编译器已经可以假定 p1 != p2 因为它的类型不同(尽管是 CIS)并基于此进行优化?在我看来,这不是很好/合乎逻辑 从标准的一侧如果程序试图通过以下类型之一的glvalue访问对象的存储值,则行为未定义..和当我们通过具有 CIS 类型(即使是完整的 CS 类型)的类型访问对象时,这里正式未列出?所以UB?但从另一边联合段CIS规则。但是当我们访问“非活动”联合成员时 - 我们访问具有“活动”成员动态类型的对象的存储值 - 从我这里看,规则中的矛盾 @RbMm:我认为 6.5p7 有意义的唯一方法是认识到它仅适用于涉及别名的情况如书面,并且使用新派生的指针访问对象不是“别名”。在标准中的“坏” CIS 示例中,如果两个指针都标识同一个联合对象的成员,那么当另一个从该联合派生时,首先派生的那个将不再是“新鲜的”。然而,Clang 和 gcc 将重写不使用别名的示例,以添加他们无法处理的别名。 @RbMm:如果通过新派生指针的访问被识别为对派生对象的访问,那么很少有程序需要访问具有不相关成员类型左值的结构和联合的权限(或字符类型,就此而言!),因此缺乏一揽子许可是有道理的。不幸的是,由于标准的作者没有说高质量的编译器应该识别指针派生(可能认为关于别名的脚注就足够了),clang 和 gcc 不会打扰。 从另一边 - this code 正式正确吗?这里 3 个不同的 union 成员没有 CIS 而是从意义上说 - 所有 3 个都是指针,具有相同的内存布局,所以我们可以在分配/更改后读取任何一个

以上是关于为啥在一个函数中声明的联合类型在另一个函数中使用无效?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能在另一个函数中定义一个函数?

为啥这是 C++ 中的有效函数声明? [复制]

在另一个函数中使用在函数内部声明的向量

Cgo 可以调用在另一个目录中声明的 C 函数吗?

为啥pugixml解析的数据会丢失在另一个函数中?

标C编程笔记day05 函数声明文件读写联合类型枚举类型