严格的别名规则指定不正确吗?
收藏

如先前建立的形式的并集

union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

具有n个成员的对象在重叠存储中包含n +1个对象:一个对象用于联合本身,每个对象对应一个对象。显然,即使读取的工会成员不是最后一个写入的工会成员,您也可以按任意顺序自由地对其进行读写。绝对不会违反严格的别名规则,因为访问存储所使用的左值具有正确的有效类型。

脚注95进一步支持了这一点,脚注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);
}

because it can safely assume that the write to *f does not affect the value of *i.

However, what happens when we pass two pointers to members of the same union? Consider this example, assuming a typical platform where float is an IEEE 754 single precision floating point number and int is a 32 bit two's complement integer:

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

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

As previously established, fi.i and fi.f refer to an overlapping memory region. Reading and writing them is unconditionally legal (writing is only legal once the union has been initialized) in any order. In my opinion, the previously discussed optimization performed by all major compilers yields incorrect code as the two pointers of different type legally point to the same location.

我不敢相信我对严格的别名规则的解释是正确的。由于前面提到的极端情况,不可能为严格的混叠设计最佳的优化是不可能的。

请告诉我为什么我错了。

在研究过程中出现了一个相关的问题。

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

最佳答案

从您的示例开始:

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

Let's first acknowledge that, in the absence of any unions, this would violate the strict aliasing rule if i and f both point to the same object; assuming the object has no effective type, then *i = 1 sets the effective type to int and *f = 1.0 then sets it to float, and the final return (*i) then accesses an object with effective type of float via an lvalue of type int, which is clearly not allowed.

The question is about whether this would still amount to a strict-aliasing violation if both i and f point to members of the same union. On union member access via the "." member access operator, the specification says (6.5.2.3):

后缀表达式,后跟。运算符和标识符   指定结构或联合对象的成员。价值在于   命名成员(95)的值,如果第一个表达式是   一个左值。

上面提到的脚注95说:

如果用于读取联合对象内容的成员不是   与上一次用于在对象中存储值的成员相同,   对象表示的适当部分的值是   重新描述为新类型的对象表示形式,如所述   在6.2.6中(有时称为“类型校正”的过程)。这可能是   陷阱表示。

显然,这旨在允许通过联合进行类型修饰,但应注意:(1)脚注是非规范性的,也就是说,脚注不应禁止行为,而应澄清脚注的某些部分的意图。 (2)编译器供应商认为通过联合进行类型修剪的这种允许仅被视为通过联合成员访问运算符申请访问-因为否则严格的别名是毫无意义的,因为任何潜在的别名访问也可能是同一联合的成员。

您的示例通过指向不存在的或至少非活动的联合成员的指针进行存储,从而实施了严格的别名冲突(因为它使用不合适的类型的左值访问了活动的成员)或使用了不表示对象(因为与非活动成员相对应的对象不存在)-可以用任何一种方式争论,并且标准也不明确,但是两种解释都意味着您的示例具有未定义的行为。

(我可能会补充说,我看不到脚注允许通过联合进行类型修饰的方式如何描述规范中固有的行为-也就是说,这似乎违反了ISO禁止行为的规则;规范中没有其他内容以便允许通过联合进行类型修饰。此外,阅读规范文本有点费力,因为要求这种形式的类型转换要求必须立即通过联合类型进行访问。

规范的另一部分经常引起混乱,但是,在6.5.2.3中也是如此:

为了简化联合的使用,做出了一项特殊保证:   如果一个联合包含几个共享共同的缩写的结构   序列(请参见下文),以及联合对象当前是否包含一个   在这些结构中,可以检查共同的姓名缩写   其中任何一部分的完成类型声明的一部分   工会的形象可见。

尽管由于没有通用的初始序列,所以这不适用于您的示例,但我已经看到人们认为这是控制类型修剪的通用规则(至少在涉及通用的初始序列时);他们认为,这意味着应该有可能使用这样的类型修剪,只要在完整的联合声明可见时就可以使用两个指向不同联合成员的指针(因为在上面引用的段落中出现了这种含义的词)。但是,我要指出的是,以上段落仍仅适用于通过“。”进行的工会成员访问。操作员。在这种情况下,协调这种理解的问题是,无论如何,完整的工会声明必须始终可见,因为否则您将无法引用工会成员。我认为这是措辞上的小故障,再加上示例3中的类似措辞不佳(以下内容不是有效的片段(因为联合体类型不可见...),而联合体可见性并不是真正的决定因素) ,这使某些人相信common-initial-sequence异常旨在全局应用,而不仅仅是通过“。”进行成员访问。运算符,作为严格别名规则的例外;并且得出这个结论后,读者可能会解释有关类型修剪的脚注也要在全球范围内应用,有些人会这样做:例如,请参见有关此GCC错误的讨论(请注意,该错误已处于SUSPENDED状态很长时间了) )。

(顺便说一下,我知道有几个编译器没有实现“全局通用初始序列”规则。我没有具体意识到任何实现“全局通用初始序列”规则的编译器,同时也不允许任意类型的修剪,但是委员会对缺陷报告257的答复表明,他们希望规则是全球性的,但是,我个人认为,仅类型的可见性应该改变代码的语义,并不是指这种类型存在严重缺陷,我知道其他人也同意)。

At this point you could well question how reading a non-active union member via the member-access operator doesn't violate strict aliasing, if doing the same via a pointer does so. This is again an area where the specification is somewhat hazy; the key is perhaps in deciding which lvalue is responsible for the access. For instance, if a union object u has a member a and I read it via the expression u.a, then we could interpret this as either an access of the member object (a) or as merely an access of the union object (u) which the member value is then extracted from. In the latter case, there is no aliasing violation since it is specifically allowed to access an object (i.e. the active member object) via an lvalue of aggregate type containing a suitable member (6.5¶7). Indeed, the definition of the member access operator in 6.5.2.3 does support this interpretation, if somewhat weakly: the value is that of the named member - while it is potentially an lvalue, it is not necessary to access the object referred to by that lvalue in order to obtain the value of the member, and so strict aliasing violation is avoided. But this is again stretching a little.

(在我看来,通常来说,当对象按照“6.5¶7”具有“通过左值表达式访问其存储的值...”时,这种规定似乎不够明确;我们当然可以为自己做出合理的确定,但是我们必须请小心允许通过以上所述的联合进行类型打折,否则将不理会脚注95。尽管通常会产生不必要的麻烦,但有时仍缺少必要的详细说明。

在某些时候,关于联合语义的争论总是引用DR 236。实际上,您的示例代码表面上与该缺陷报告中的代码非常相似。我会指出:

  1. “委员会认为示例2违反了6.5第7段中的别名规则” –这与我上面的推理并不矛盾;
  2. “为了不违反规则,示例中的函数f应该写为”-这支持我上面的推理;您必须使用并集对象(和“。”运算符)来更改活动成员类型,否则,您正在访问不存在的成员(因为一次只能包含一个成员);
  3. DR 236中的示例与类型处理无关。这是关于是否可以通过指向非活动联盟成员的指针来分配该成员的确定。所讨论的代码与此处所讨论的代码略有不同,因为它在写入第二个成员之后不再尝试再次访问“原始”联合成员。因此,尽管示例代码在结构上相似,但缺陷报告在很大程度上与您的问题无关。
  4. DR 236中的委员会答复声称“两个程序都调用未定义的行为”。但是,讨论不支持此操作,该讨论仅显示示例2调用了未定义的行为。我相信回应是错误的。

    公众号
    关注公众号订阅更多技术干货!