是否错误地指定了严格的别名规则?

Is the strict aliasing rule incorrectly specified?

作为 ,形式为

的并集
union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

with n 成员包含 n + 1 个重叠存储中的对象:联合本身一个对象,每个联合成员一个对象.很明显,您可以自由地以任何顺序读写任何工会成员,即使阅读的工会成员不是最后一个写信给的工会成员。永远不会违反严格的别名规则,因为您访问存储的左值具有正确的有效类型。

这是脚注 95 的 ,它解释了类型双关如何成为联合的预期用途。

严格别名规则启用的优化的典型示例是此函数:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

编译器可能会优化为

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (1);
}

因为它可以安全地假设对 *f 的写入不会影响 *i 的值。

但是,当我们将两个指针传递给同一个联合体的成员时会发生什么?考虑这个例子,假设一个典型的平台,其中 float 是一个 IEEE 754 单精度浮点数,int 是一个 32 位二进制补码整数:

int breaking_example(void)
{
    union {
        int i;
        float f;
    } fi;

    return (strict_aliasing_example(&fi.i, &fi.f));
}

如前所述,fi.ifi.f 指的是重叠的内存区域。以任何顺序读写它们都是无条件合法的(写入只有在联合初始化后才合法)。在我看来,所有主要编译器执行的先前讨论的优化都会产生不正确的代码,因为不同类型的两个指针合法地指向同一位置。

我无法相信我对严格别名规则的解释是正确的。由于上述极端情况,严格别名设计的优化不可能实现,这似乎不太合理。

请告诉我为什么我错了。

在研究过程中出现了 related question

请在添加您自己的答案之前阅读所有现有答案及其评论,以确保您的答案添加了新的论点。

这是注释 95 及其上下文:

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member, (95) and is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

(95) If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). This might be a trap representation.

注释 95 明确适用于通过工会成员的访问。您的代码不会那样做。通过指向 2 个不同类型的指针访问两个重叠对象,其中 none 是字符类型,none 是与类型双关相关的后缀表达式。

这不是一个确定的答案...

C11 标准(§6.5.2.3.9 示例 3)有以下示例:

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

但我找不到关于此的更多说明。

根据§6.5.2.3中联合成员的定义:

3 A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. ...

4 A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. ...

另见§6.2.3¶1:

  • the members of structures or unions; each structure or union has a separate name space for its members (disambiguated by the type of the expression used to access the member via the . or -> operator);

很明显,脚注95指的是在联合范围内使用.->运算符访问联合成员。

由于对组成联合的字节的分配和访问不是通过联合成员而是通过指针进行的,因此您的程序不会调用联合成员的别名规则(包括脚注 95 阐明的那些规则)。

此外,由于 *f = 1.0 之后的对象的有效类型是 float,因此违反了正常的别名规则,但它的存储值是由类型 int 的左值访问的(参见§6.5¶7).

注意:所有参考文献均引用 this C11 标准草案。

本质上,严格别名规则描述了允许编译器假设(或者相反,不允许假设)两个不同类型的指针不指向内存中相同位置的情况。

在此基础上,您在 strict_aliasing_example() 中描述的优化是允许的,因为允许编译器假定 fi 指向不同的地址。

breaking_example() 导致传递给 strict_aliasing_example() 的两个指针指向同一个地址。这打破了允许 strict_aliasing_example() 的假设,因此导致该函数表现出未定义的行为。

因此您描述的编译器行为是有效的。事实上 breaking_example() 导致传递给 strict_aliasing_example() 的指针指向同一地址,从而导致未定义的行为 - 换句话说,breaking_example() 打破了允许编译器进行的假设strict_aliasing_example().

以内

让我们暂时离开标准,思考一下编译器的实际可能性。

假设在strict_aliasing_example.c中定义了strict_aliasing_example(),在breaking_example.c中定义了breaking_example()。假设这两个文件分别编译然后链接在一起,如下所示:

gcc -c -o strict_aliasing_example.o strict_aliasing_example.c
gcc -c -o breaking_example.o breaking_example.c
gcc -o breaking_example strict_aliasing_example.o breaking_example.o

当然我们还得给breaking_example.c添加一个函数原型,看起来像这样:

int strict_aliasing_example(int *i, float *f);

现在考虑gcc的前两次调用是完全独立的,除了函数原型外不能共享信息。编译器在为strict_aliasing_example()生成代码时不可能知道ij会指向同一个联合的成员。链接或类型系统中没有任何内容可以指定这些指针在某种程度上是特殊的,因为它们来自联合。

这支持其他答案提到的结论:从标准的角度来看,通过 .-> 访问联合与取消引用任意指针相比遵守不同的别名规则。

严格的别名规则禁止两个不兼容类型的指针访问同一个对象,除非一个是指向字符类型的指针:

7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.

在您的示例中,*f = 1.0; 正在修改 fi.i,但类型不兼容。

我认为错误在于认为联合包含 n 个对象,其中 n 是成员数。根据§6.7.2.1 ¶16

,联合在程序执行期间的任何时刻仅包含一个活动对象

The value of at most one of the members can be stored in a union object at any time.

可以在 §6.5.2.3 中找到对联合不会同时包含其所有成员对象的解释的支持:

and if the union object currently contains one of these structures

最后,2006 年 defect report 236 提出了一个几乎相同的问题。

Example 2

// optimization opportunities if "qi" does not alias "qd"
void f(int *qi, double *qd) {
    int i = *qi + 2;
    *qd = 3.1;       // hoist this assignment to top of function???
    *qd *= i;
    return;
}  

main() {
    union tag {
        int mi;
        double md;
    } u;
    u.mi = 7;
    f(&u.mi, &u.md);
}

Committee believes that Example 2 violates the aliasing rules in 6.5 paragraph 7:

"an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)."

In order to not violate the rules, function f in example should be written as:

union tag {
    int mi;
    double md;
} u;

void f(int *qi, double *qd) {
    int i = *qi + 2;
    u.md = 3.1;   // union type must be used when changing effective type
    *qd *= i;
    return;
}

从你的例子开始:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

让我们首先承认,在没有任何联合的情况下,如果 if 都指向同一个对象,这将违反严格的别名规则;假设对象没有声明类型,那么 *i = 1 将有效类型设置为 int 并且 *f = 1.0 然后将其设置为 float,最后的 return (*i) 然后访问通过类型 int 的左值具有有效类型 float 的对象,这显然是不允许的。

问题是,如果 if 都指向同一个联合的成员,这是否仍然构成严格的别名违规。如果情况并非如此,则必须是在这种情况下适用的严格别名规则有一些特殊豁免,或者通过 *i 访问对象不会(也)访问相同的对象对象为 *f.

关于通过“.”访问工会成员。成员访问运算符,标准说 (6.5.2.3):

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member (95) and is an lvalue if the first expression is an lvalue.

上面提到的脚注95说:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

这显然是为了允许通过联合进行类型双关,但应该注意的是 (1) 脚注是非规范性的,也就是说,它们不应该禁止行为,而是应该阐明意图文本的某些部分符合规范的其余部分,以及 (2) 这种通过联合进行类型双关的允许被编译器供应商视为应用 仅适用于通过联合成员访问运算符 [= 访问90=] - 因为否则严格的别名对于优化是毫无用处的,因为几乎任何两个指针都可能引用同一联合的不同成员(你的例子就是一个很好的例子)。

所以在这一点上,我们可以说:

  • 非规范脚注明确允许您示例中的代码
  • 另一方面,规范文本似乎不允许您的示例(由于严格的别名),假设访问联合会的一个成员也构成对另一个成员的访问 - 但稍后会详细介绍

但是,访问联合的一个成员实际上是否访问了其他成员?如果不是,则严格的别名规则与该示例无关。 (如果是这样,严格的别名规则,有问题的是,不允许通过联合进行任何类型双关)。

联合定义为(6.2.5 第 20 段):

A union type describes an overlapping nonempty set of member objects

并注意(6.7.2.1 第 16 段):

The value of at most one of the members can be stored in a union object at any time

因为 access 是 (3):

〈execution-time action〉 to read or modify the value of an object

...并且,由于非活跃的联合成员没有存储值,因此大概访问一个成员并不构成对其他成员的访问!

然而,成员访问的定义(6.5.2.3,上面引用)说“值是指定成员的值”(这是脚注 95 所附的准确声明)- 如果成员没有价值,然后呢?脚注 95 给出了答案,但正如我所指出的,规范文本不支持它。

无论如何,文中没有任何内容似乎暗示“通过成员对象”读取或修改联合成员(即直接通过使用成员访问运算符的表达式)应该与读取或修改有任何不同它通过指向同一成员的指针。标准文本不支持编译器供应商应用的共识理解,允许他们在不同类型的指针不别名的假设下执行优化,并且要求类型双关只能通过涉及成员访问的表达式执行。

如果脚注 95 被认为是规范的,那么根据文本的其余部分,您的示例是没有未定义行为的完美代码(除非 (*i) 的值是陷阱表示)。但是,如果脚注 95 不被视为规范,则会尝试访问没有存储值的对象,并且行为充其量是不清楚的(尽管严格的别名规则可以说是不相关的)。

根据目前编译器供应商的理解,您的示例具有未定义的行为,但由于标准中未指定,因此尚不清楚代码违反了哪些约束。

就我个人而言,我认为标准的“修正”是:

  • 禁止访问非活动联合成员,除非通过成员访问表达式的左值转换,或通过左侧为成员访问表达式的赋值(对此可能有例外)有问题的成员具有字符类型,因为由于严格别名规则本身中的类似例外,这不会对可能的优化产生影响)
  • 在规范性文本中指定非活跃成员的价值如脚注 95 当前所述

这将使您的示例不违反严格的别名规则,而是违反必须仅通过包含成员访问运算符(和适当的成员)的表达式访问非活动联合成员的约束.

因此,回答您的问题 - 是否错误地指定了严格的别名规则? - 不,严格的别名规则不相关对于这个例子,因为两个指针解引用访问的对象是单独的对象,即使它们在存储中重叠,一次也只有一个有值。但是,联合成员访问规则指定不正确。

关于缺陷报告 236 的注释:

关于联合语义的争论在某些时候总是参考 DR 236。事实上,您的示例代码在表面上与该缺陷报告中的代码非常相似。我会注意到:

  1. DR 236 中的示例不是关于类型双关的。这是关于是否可以通过指向该成员的指针分配给非活动联合成员。所讨论的代码与此处的问题略有不同,因为它不会在写入第二个成员后再次尝试访问“原始”联合成员。因此,尽管示例代码的结构相似,但缺陷报告在很大程度上与您的问题无关。
  2. “委员会认为示例 2 违反了 6.5 第 7 段中的别名规则”——这表明委员会认为编写一个“非活动”联合成员,而不是通过包含联合成员访问权限的表达式对象,是一个严格的别名违规。正如我在上面详述的那样,标准的文本不支持这一点。
  3. “为了不违反规则,示例中的函数 f 应写为” - 即您必须使用联合对象(和“.”运算符)来更改活动成员类型;这与我在上面提出的标准的“修复”是一致的。
  4. 委员会在 DR 236 中的回应声称“两个程序都会调用未定义的行为”。它没有解释为什么第一个这样做,它对为什么第二个这样做的解释似乎是错误的。

在 C89 标准之前,绝大多数实现将对特定类型指针的写取消引用行为定义为以为该类型定义的方式设置底层存储的位,并定义了行为读取取消引用特定类型的指针,以按为该类型定义的方式读取底层存储的位。虽然这些能力并非对所有实现都有用,但有许多实现可以通过例如使用 32 位加载和存储一次对四个字节的组进行操作。此外,在许多此类实现中,支持此类行为不会花费任何成本。

C89 标准的作者声明,他们的目标之一是避免不可挽回地破坏现有代码,并且有两种基本方式可以解释规则:

  1. C89 规则原本打算仅适用于与基本原理中给出的情况类似的情况(直接通过该类型和通过指针间接访问具有声明类型的对象), 编译器没有理由认为左值是相关的。跟踪每个变量是否当前缓存在寄存器中非常简单,并且能够在访问其他类型的指针时将这些变量保存在寄存器中是一种简单而有用的优化,并且不会排除对使用更常见的代码的支持类型双关模式(让编译器将 float* 解释为 int* 转换为需要刷新任何寄存器缓存的 float 值是简单明了的;这种转换非常罕见,以至于这种方法不太可能对性能产生不利影响)。

  2. 鉴于标准对于给定平台的高质量实现的要素通常是不可知的,因此可以将规则解释为允许实现破坏以以下方式使用类型双关的代码将既有用又明显,而不是建议高质量的实现不应该试图避免这样做。

如果标准定义了一种允许 就地 类型双关的实用方法,该方法在任何方面都不会明显劣于其他方法,那么定义方法以外的方法可能合理被视为弃用。如果不存在标准定义的方法,那么对于需要类型双关以实现良好性能的平台的质量实现应该努力有效地支持这些平台上的通用模式,无论标准是否要求它们这样做。

不幸的是,标准要求的不明确导致了一些人认为不推荐使用的结构,没有替代品。存在涉及两个基本类型的完整联合类型定义被解释为指示通过一种类型的指针进行的任何访问都应被视为对另一种类型的可能访问,这将使调整依赖于就地类型的程序成为可能在没有未定义行为的情况下这样做是双关语——根据目前的标准,这是任何其他实用方法都无法实现的。不幸的是,这样的解释也会在 99% 的无害情况下限制许多优化,从而使以这种方式解释标准的编译器无法尽可能高效地 运行 现有代码。

至于是否正确指定了规则,那将取决于它应该是什么意思。多种合理的解释是可能的,但将它们结合起来会产生一些相当不合理的结果。

PS--关于指针比较和 memcpy 的规则的唯一解释是在不给术语 "object" 一个不同于其在别名中的含义的情况下有意义的规则建议没有分配的区域可以用来容纳超过一种的对象。虽然某些类型的代码可能能够遵守这样的限制,但如果没有过多的 malloc/free 调用,程序将无法使用自己的内存管理逻辑来回收存储。该标准的作者可能打算说 不需要 实现让程序员创建一个大区域并将其分成较小的混合类型块,但这并不意味着他们希望通用实现无法做到这一点。

标准不允许使用成员类型的左值访问结构或联合的存储值。由于您的示例使用类型不是联合的左值或包含该联合的任何类型的左值访问联合的存储值,因此仅在该基础上行为将是未定义的。

一件棘手的事情是,在严格阅读标准的情况下,即使是像

这样简单的东西
int main(void)
{
  struct { int x; } foo;
  foo.x = 1;
  return 0;
}

也违反了N1570 6.5p7,因为foo.xint类型的左值,用于访问struct foo类型对象的存储值,int 不满足该部分的任何条件。

如果人们认识到在涉及从其他左值派生的左值的情况下需要有 N1570 6.5p7 的例外情况,则该标准甚至可以远程使用的唯一方法。如果标准要描述编译器可能或必须识别此类推导的情况,并指定 N1570 6.5p7 仅适用于在函数或循环的特定执行中使用多种类型访问存储的情况,那将消除很多复杂性,包括对 "Effective Type".

概念的任何需求

不幸的是,即使在一些明显的情况下,一些编译器也会自行忽略左值和指针的推导,例如:

s1 *p1 = &unionArr[i].v1;
p1->x ++;

如果涉及 unionArr[i] 的其他操作将 p1 的创建和使用分开,那么编译器无法识别 p1unionArr[i].v1 之间的关联可能是合理的,但 gcc 都没有即使在使用指针 立即 遵循获取联合成员地址的操作的简单情况下,clang 也无法始终如一地识别这种关联。

同样,由于标准不要求编译器识别派生左值的任何用法,除非它们是字符类型,gcc 和 clang 的行为不会使它们成为 non-conforming。另一方面,他们符合标准的唯一原因是标准中的一个缺陷,这个缺陷非常离谱,以至于没有人阅读标准说明它实际做了什么。