【问题标题】:Strict aliasing in relation to aggregate or union types与聚合或联合类型相关的严格别名
【发布时间】:2015-02-10 14:40:36
【问题描述】:

我试图了解 C99 标准 (C99; ISO/IEC 9899:1999 6.5/7) 中以下语句的含义

对象的存储值只能由左值访问 具有以下类型 73) 或 88) 之一的表达式:

  • (省略其他与问题无关的陈述)

  • 聚合或联合类型,包括上述之一 其成员之间的类型(包括,递归地,一个成员 子聚合或包含联合)

考虑以下几点:

typedef struct MyComplex {
    float real;
    float imag;
} MyComplex;

MyComplex *carray = malloc(sizeof(*carray) * 10);
float *as_floats = (float *)carray;

这是否合法,因为 MyComplex 结构包含与 as_floats 指针兼容的 float 类型?

反过来呢?即:

float *farray = malloc(sizeof(*farray) * 10);
MyComplex *as_complex = (MyComplex *)farray;

在这两种情况下,最终,我们在这里处理的所有内容都是float,所以也许没关系?我只是不确定。

我问,因为我正在处理一个遗留代码库,它在所有地方都做这种事情,到目前为止,一切似乎都很好。但我觉得我们在这里玩火。试图弄清楚我是否需​​要在编译器命令行上禁用严格别名。

【问题讨论】:

  • 应该MyComplex *carray = malloc(sizeof(*carray) * 10);MyComplex *carray = malloc(sizeof(MyComplex) * 10); 吗?
  • 不,它是等价的,除非carray 的类型发生变化时它不会中断。
  • 我认为事情并不总是那么理想。事实上,在必要时添加填充对于实现是有好处的,因此这两种方式都可能导致读取/写入垃圾数据。

标签: c gcc standards c99 strict-aliasing


【解决方案1】:

(我相信)标准的所有版本的 6.7.2.1 中的注释 13 都明确地宽恕了案例 #1。你很少会得到更明确的答案! 我的重点

在结构对象中,非位域成员和单元 哪些位域的地址按顺序增加 他们被宣布。 一个指向结构对象的指针,适当地 转换后,指向其初始成员(或者如果该成员是 位域,然后到它所在的单元),反之亦然。 结构对象内可能有未命名的填充,但在其 开始。

http://port70.net/~nsz/c/c99/n1256.html#6.7.2.1

是的!您可以强制转换结构以直接访问其第一个成员! 为什么有人认为这比&(carray->real) 更好尚不清楚。但这绝对是合法的。

正如另一位评论者指出的那样,我在上一个问题中讨论了案例 #2。

Aliasing Arrays through structs

总结起来,案例#2 是访问成员real 的好方法。 此外,如果结构没有内部填充(取决于平台),您甚至可以通过 imag 访问数组的第二个成员。

我说依赖于平台,但其他调查没有提供这样的结构包含填充的已知平台。

【讨论】:

  • 非常有用,谢谢!没有考虑结构可能包含填充的情况!
  • @epicbrew (据我所知)没有已知平台可以让您的结构包含填充!我很想听听一个。从来没有人有义务。因此,出于实际目的,这不是问题-在您非常狭窄的情况下。那是因为结构中的所有成员都是同一类型。
  • 是的,同意,但仍然很高兴了解更一般情况下的潜在陷阱。 :-)
【解决方案2】:

我尝试重现使用这种铸造方法的一些问题。 一个问题是当 struct 使用位字段时,另一个问题 - 当存在强制 struct 成员对齐到某个大小的编译器指令时:

// issue 1 : problem with bitFields
typedef struct MyComplex_1 {
    unsigned int real : 1;
    unsigned int imag : 2;
} MyComplex_1;

// issue 2 : problem with members forced alignment
typedef struct MyComplex_2 {
    unsigned int real;
    unsigned int imag;
} __attribute__ ((aligned (64))) MyComplex_2; // GCC compiler directive

int main()
{
    MyComplex_1 arr_1[] = {{1, 2}, {1, 2}};
    MyComplex_2 arr_2[] = {{1, 2}, {1, 2}};

    int i;

    // for bitfield issue
    for (i=0; i<2; i++)
        printf("struct (%d,%d)\n", arr_1[i].real, arr_1[i].imag);

    for (i=0; i<4; i++)
        printf("var (%d)\n", ((unsigned int*)arr_1)[i]);

    // for struct member forced alignement issue
    for (i=0; i<2; i++)
        printf("struct (%d,%d)\n", arr_2[i].real, arr_2[i].imag);

    for (i=0; i<4; i++)
        printf("var (%d)\n", ((unsigned int*)arr_2)[i]);

    return 0;
}

可能还有更多的问题,但在我看来这件事是有风险的。

【讨论】:

  • 感谢您的示例。同意这种类型的选角并不理想。
  • 没问题。 C 是双刃剑,您可以制作手头的每个演员表并获得良好的性能是件好事,但相反 - 如果使用过于广泛,通用演员表可能会很麻烦(请参阅我个人资料中的笑话 :-))。
  • @AgniusVasiliauskas:Classic C 让你带上自己的安全装备,如果不是 100% 有效,它会让你撞到脚趾。新改进的超现代 C 认识到任何安全装置的无用性,这些安全装置只会减少对脚趾的致命伤害,而不是防止所有伤害,并且通过完全消除这些无用的安全装置来提高代码效率。
【解决方案3】:

我同意你在玩火,但不是因为你引用的标准部分。

任何对象,包括作为聚合类型(数组或结构)对象的组件的对象,都以地址和类型为特征。给出声明后,考虑这些形式的访问(对于 0

/* 1 */
carray[n].real

/* 2 */
asfloats[n * 2]

标准规定上述两种形式都可以访问float对象,但没有说明它们是否都引用了相同的对象,或者后者是否引用了任何对象一点也不。它们可能都被允许并且可能甚至意味着相同的东西,但这取决于struct MyComplex的实现定义的表示(除了当n为零时,两者总是被允许的并且是等价的)。

例如,如果float 具有四字节表示,并且编译器恰好将struct 表示填充为 16 字节的倍数,那么类型 (2) 的某些访问(旨在对 @通过float *的987654327@成员将无效。

访问imag 成员更糟糕:如果编译器选择在 8 字节边界上对齐每个成员,那么您也可能会受到影响,因此需要在 realimag 之间进行填充。

还要注意,填充甚至不一定是实现的固定特征。许多编译器提供了影响其填充决策的选项,因此您正在使用的代码是否具有已定义的行为可能取决于您的编译器选项。

【讨论】:

  • 是的,填充物看起来确实是一个有效的问题。不是说我宽恕它,但在某些平台上,通过指示编译器不要填充 MyComplex 结构(即 gcc 的pack() pragma)可能会绕过这个问题。虽然不是我想要依赖的东西......
  • 代码可以使用编译时offsetof 检查来确保对象具有相同的地址。然而,没有办法确保给定两个指向相同类型联合的指针的编译器是否会识别&amp;u1-&gt;member1 可能别名&amp;u2-&gt;member2,即使在最后一次使用前一个指针之间评估后一个表达式的情况下以及后者的第一次使用。
猜你喜欢
  • 1970-01-01
  • 2019-11-14
  • 2020-08-01
  • 2016-11-21
  • 2012-03-27
  • 1970-01-01
  • 1970-01-01
  • 2012-10-14
相关资源
最近更新 更多