为什么在一个函数中声明的union类型在另一个函数中使用它无效?

kan*_*wei 38 c scope function unions

当我阅读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);
}
Run Code Online (Sandbox Code Playgroud)

我测试时发现没有错误和警告.

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

Sto*_*ica 33

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

6.5.2.36

一个特殊的担保是为了简化使用工会提出:如果一个联合包含共享一个公共初始序列几种结构(见下文),而如果联合对象当前包含这些结构中的一个,它被允许检查常见其中任何一个的初始部分都可以看到完整类型的联合声明.如果对应的成员具有一个或多个初始成员的序列的兼容类型(并且对于位字段,具有相同的宽度),则两个结构共享共同的初始序列.

由于f之前已声明g,并且未命名的联合类型是本地的g,因此毫无疑问,联合类型不可见f.

该示例未显示如何u初始化,但假设最后写入成员u.s2.m,该函数具有未定义的行为,因为它检查p1->m没有共同的初始序列保证生效.

另一方面,如果它u.s1.m是在函数调用之前最后写入的,那么访问p2->m是未定义的行为.

请注意,f它本身并非无效.这是一个非常合理的函数定义.未定义的行为源于传入它&u.s1&u.s2作为参数.这就是导致未定义行为的原因.


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

  • @Zastai - 你知道,我不确定.我认为这是一个值得站立的好问题.使用[tag:language-lawyer]标签发布.应该很有趣. (5认同)
  • @haccks - 是的.这就是为什么GCC和Clang在严格别名的情况下进入这个功能的原因.他们没有理由假设两个不相关的类型可能是别名.因为通常他们不能.从技术上讲,这都是在"有效类型"条款下处理的.但[这是另一个例子](https://godbolt.org/z/m11ZvZ).这里编译器知道混叠是可能的,所以它不是太激进. (2认同)

RbM*_*bMm 26

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

这个功能

int f(struct t1* p1, struct t2* p2);
Run Code Online (Sandbox Code Playgroud)

假设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);
}
Run Code Online (Sandbox Code Playgroud)

必须g返回什么?+1根据常识(我们改变-1到+1 in f).但是如果我们看一下gcc的生成程序集并进行-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
Run Code Online (Sandbox Code Playgroud)

到目前为止,所有都是例外.但是当我们尝试时-O2

f:
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        js      .L4
        ret
.L4:
        neg     DWORD PTR [rsi]
        ret
g:
        mov     eax, -1
        ret
Run Code Online (Sandbox Code Playgroud)

返回值现在是硬编码的 -1

这是因为f在开始高速缓存的值p1->meax寄存器(mov eax, DWORD PTR [rdi])和不重读它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; 
}
Run Code Online (Sandbox Code Playgroud)

编译器缓存p1->m值在本地var中a(当然实际上在寄存器中)并返回它,尽管有p2->m = -p2->m;变化p1->m.但编译器假设p1内存不受影响,因为它假设p2指向另一个不重叠的内存p1

因此,对于不同的编译器和不同的优化级别,相同的源代码可以返回不同的值(-1或+1).这样和未定义的行为