【问题标题】:Exception to strict aliasing rule in C from 6.5.2.3 Structure and union members来自 6.5.2.3 结构和联合成员的 C 中严格别名规则的例外
【发布时间】:2014-01-17 00:44:01
【问题描述】:

引用自 C99 标准:

6.5.2.3

5 一个特殊的保证是为了简化联合的使用:如果联合包含多个共享相同初始序列的结构(见下文),并且如果联合对象当前包含这些结构之一,允许在任何可见联合的完整类型声明的任何地方检查其中任何一个的公共初始部分。如果相应的成员具有兼容的类型,则两个结构共享一个公共初始序列(并且,对于 bit-字段,宽度相同)用于一个或多个初始成员的序列。

有这个案例的例子:

// The following code is not a valid fragment because
// the union type is not visible within the function 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);
}

我添加了一些更改:

#include <stdio.h>

struct t1 { int m; };
struct t2 { int m; };

union u
{
    struct t1 s1;
    struct t2 s2;
};

int foo(struct t1 *p1, struct t2 *p2)
{
    if (p1->m)
        p2->m = 2;
    return p1->m;
}

int main(void)
{
    union u u;
    u.s1.m = 1;
    printf("%d\n", foo(&u.s1, &u.s2));
}

如您所见,我已将联合声明移到外部,以便在 foo() 中可见。根据标准的评论,这应该使我的代码正确,但看起来严格的别名仍然会破坏 clang 3.4 和 gcc 4.8.2 的代码。

使用 -O0 输出:

2

-O2 的输出:

1

对于两个编译器。

所以我的问题是:

C 真的依赖联合声明来决定某些结构是否是严格别名规则的例外吗?还是 gcc/clang 都有 bug?

这对我来说似乎真的很糟糕,因为即使函数和联合都在同一个标​​头中声明,这并不能保证联合在带有函数主体的翻译单元中可见。

【问题讨论】:

  • 使用 gcc 4.4.7 输出 2
  • 在另一个线程中检查我的未答复,尤其是 GCC 邮件列表讨论的链接:stackoverflow.com/a/19807355/1546653 我也非常希望看到这一点得到澄清。
  • 您首先给出的示例:是否超出标准?它看起来不像标准所说的示例?我认为标准说如果你在“main”中做,“u.s1.m = 5”,那么编译器必须假设“u.s2.m”已经改变;但如果你传递指向“u.s1”和“u.s2”的指针,那就完全不同了。
  • 标准语言清晰;联合声明中存在两个结构应被视为通知编译器代码将覆盖这两种类型,因此公共初始序列的成员可能会别名。 gcc 的作者不喜欢标准所说的,并选择忽略它,但我可以想象标准没有其他理由提到 complete 类型如果这种可见性被认为不够充分,则可见,请注意可能出现混叠。

标签: c struct unions pointer-aliasing


【解决方案1】:

最重要的一点是您的更改(向上移动联合)根本没有更改函数foo 的定义。它仍然是一个接收无关指针的函数。在您的示例中,传递的指针是相关的,而在其他地方可能会有所不同。编译器的目标是服务于最一般的情况。修改后函数体不一样了,不清楚原因。

您要问的问题是在您的特定编译器中对某些命令行键进行了多么仔细的优化。它与内存布局无关。在正确的编译器中,结果应该是相同的。当 2 个不同的指针实际上指向内存中的同一位置时,编译器应该处理这种情况。

【讨论】:

  • 标准谈论工会而不是其他任何东西。你的foo 没有任何联合...这些东西是无关的。
  • @ChronoKitsune:Dennis Ritchie 设计的几乎所有 C89 之前的语言实现都允许程序员定义具有共同初始序列的多个结构类型,并使用指向其中任何一个的指针来检查任何其他。我不会将任何不提供这样做的方法的语言视为 Dennis Ritchie 设计的语言。允许联合声明作为对编译器的通知,即一种类型的指针可用于访问另一种类型的成员可能不是支持语义的特别好的方式,但我没有什么else ...
  • ...在标准中定义了一种可以实现的方法。可以肯定的是,如果标准能够明确识别 C89 的作者可能认为过于明显而不值得一提的某些模式,这也会有所帮助,这将允许大多数常见的初始序列问题 - 以及许多其他——有待更有效地解决,但目前 gcc 忽略了那些常见的模式。
  • @supercat 你说的我完全没有问题。 GCC 忽略它们,因为不清楚访问是否必须使用联合对象,或者包含这些结构的联合类型的声明是否足够。即different interpretations yield different resultsN1520 旨在澄清问题并介绍在这种情况下澄清解决方案的必要性。
  • 您可以通过另外传递一个指向联合对象的指针并手动比较&amp;up-&gt;s1 == p1,有条件地执行if (p1-&gt;m)块(或只是执行if (&amp;up-&gt;s1 == p1 &amp;&amp; p1-&gt;m) p2-&gt;m = 2;)并返回p1-&gt;m来强制执行所需的行为在末尾。这迫使 GCC 了解您完全打算使用 p2-&gt;m 写信给 up-&gt;s1.m,因为地址是相同的。当然,这违背了不将联合对象传递给f 的目的,对吧?这意味着在没有冗余的情况下可靠地执行此操作的唯一方法是将 &amp;u 本身传递给新函数 int g(union u *up)
【解决方案2】:

编译器识别对聚合成员的访问是对聚合本身的访问的情况集合纯粹是实施质量问题,标准不努力识别任何使用aggregate.memberpointerToAggregate-&gt;member 形式的非字符左值不会违反6.5p7 的情况。如果编译器至少无法处理定义的某些情况,那么编译器的质量会非常低,以至于毫无用处,但标准没有努力禁止符合但无用的实现。

如果公共初始序列成员具有​​字符类型,则 6.5p7 将定义访问它的行为,无论它是否是完整声明可见的联合的公共初始序列的成员。如果它没有字符类型,那么如果通过字符类型的左值或memcpy/memmove 执行访问,或者在目标具有堆持续时间并且使用的最终类型的情况下,则只能在 6.5p7 下定义访问for a read 匹配用于写入的类型。

质量编译器应该认识到的许多迹象表明,指向一种结构类型的指针可能用于访问另一种结构类型的 CIS 成员。无法识别任何其他指示的编译器可能会受益于将包含这两种类型的完整联合声明的存在视为此类指示。这样做可能会不必要地阻止一些原本有用的优化,但仍然比完全禁用基于类型的别名分析允许更多的优化。

【讨论】:

    猜你喜欢
    • 2018-10-27
    • 2014-07-26
    • 2021-12-22
    • 1970-01-01
    • 2019-01-29
    • 2015-05-31
    • 2015-10-15
    • 2011-02-15
    • 1970-01-01
    相关资源
    最近更新 更多