【问题标题】:Aliasing struct and array the conformant way别名结构和数组的一致方式
【发布时间】:2018-07-01 06:00:43
【问题描述】:

在 ISO C 之前的旧时代,以下代码不会让任何人感到惊讶:

struct Point {
    double x;
    double y;
    double z;
};
double dist(struct Point *p1, struct Point *p2) {
    double d2 = 0;
    double *coord1 = &p1.x;
    double *coord2 = &p2.x;
    int i;
    for (i=0; i<3; i++) {
        double d = coord2[i]  - coord1[i];    // THE problem
        d2 += d * d;
    return sqrt(d2);
}

当时,我们都知道双精度对齐允许编译器在struct Point 中不添加任何填充,我们只是假设指针算法可以完成这项工作

不幸的是,这条有问题的行使用了指针算法(p[i]定义 *(p + i)在任何数组之外,这是标准明确不允许的。 C11 的 n1570 草案在 6.5.6 加法运算符§8 中说:

当一个整数类型的表达式被添加到指针指针中或从指针指针中减去时, 结果具有指针操作数的类型。 如果指针操作数指向一个元素 一个数组对象,并且数组足够大,结果指向一个元素的偏移量 原始元素使得结果和原始的下标的差异 数组元素等于整数表达式...

当我们没有同一个数组的两个元素时什么都不说,标准未指定它并从那里未定义的行为(即使所有常见的编译器都对此感到高兴......)

问题:

由于这个习惯用法允许避免代码复制只更改xy 然后z 这很容易出错,因此浏览结构的元素就像它们是同一个数组?

免责声明:显然它只适用于相同类型的元素,并且可以使用简单的static_assert 来检测填充,如我的that other question 所示,因此填充、对齐和混合类型不是 我的问题在这里。

【问题讨论】:

  • 请注意,即使在 ISO C 之前的日子里,“双精度对齐允许编译器不添加任何填充”与“双精度对齐要求编译器避免添加任何填充”。即使在那个时候,依赖后者的代码也依赖于编译器的实现。我确信它工作可靠,而且它可能仍然工作可靠,但它不是正式未定义行为的原因是 C 的行为一开始还没有完全形式化。
  • @JohnBollinger:我知道,这就是为什么我说它会让任何人感到惊讶而不是它曾经是法律代码 ...
  • 这里是 ISO C 之前的人。不,我不会感到惊讶。我会要求犯罪者使用适当的索引变量来代替不可维护的 x、y 和 z 的临时集合。话虽如此,我会毫不犹豫地将 #define x coord[0] 之类的东西添加到当时的固定代码库中(但现在不是)。也许enum { x = 0, y = 1, z = 2 }point.coord[x]

标签: c arrays struct language-lawyer


【解决方案1】:

C 没有定义任何方式来指定编译器不得在struct Point 的命名成员之间添加填充,但是许多编译器都有一个扩展来提供这一点。如果您使用这样的扩展,或者您只是愿意假设不会有填充,那么您可以使用带有匿名内部 structunion,如下所示:

union Point {
    struct {
        double x;
        double y;
        double z;
    };
    double coords[3];
};

然后您可以通过它们各自的名称或通过coords 数组访问坐标:

double dist(union Point *p1, union Point *p2) {
    double *coord1 = p1->coords;
    double *coord2 = p2->coords;
    double d2 = 0;

    for (int i = 0; i < 3; i++) {
        double d = coord2[i]  - coord1[i];
        d2 += d * d;
    }
    return sqrt(d2);
}

int main(void) {
    // Note: I don't think the inner braces are necessary, but they silence
    //       warnings from gcc 4.8.5:
    union Point p1 = { { .x = .25,  .y = 1,  .z = 3 } };
    union Point p2;

    p2.x = 2.25;
    p2.y = -1;
    p2.z = 0;

    printf("The distance is %lf\n", dist(&p1, &p2));

    return 0;
}

【讨论】:

  • 是的,匿名结构成员很棒!
【解决方案2】:

这主要是对 JohnBollinger 的answer 的补充。匿名结构成员确实允许使用干净整洁的语法,并且 C 将联合定义为 由序列组成的类型 存储重叠的成员(6.7.2.1 结构和联合说明符§6)。然后在 6.5.2.3 结构和联合成员中指定访问联合成员:

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

和(非规范但信息丰富的)注释 95 精确:

95) 如果用于读取联合对象内容的成员与上次用于读取的成员不同 在对象中存储一个值,该值的对象表示的适当部分被重新解释 作为 6.2.6 中描述的新类型中的对象表示(有时称为“类型 双关语'')。这可能是一个陷阱表示。

这意味着对于当前版本的标准,在联合中的匿名结构成员的帮助下,数组对结构的别名是明确定义的行为。

【讨论】:

  • 除了明确定义的类型双关语之外,您还可以使用static_assert(sizeof(double[3]) == sizeof(union Point), "Weird computers not supported");防止意外的对齐打嗝
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-31
  • 1970-01-01
相关资源
最近更新 更多