从具有灵活数组成员的结构强制转换为不具有灵活数组成员的其他相同结构是未定义的行为吗?

Is it undefined behavior to cast from a struct with a flexible array member to an otherwise identical one without?

我想要一个可变大小的结构,但我想将具有特定大小的结构实例嵌入到另一个结构中。想法是这样的:

struct grid {
    size_t width, height;
    int items[ /* width * height */ ];
};

struct grid_1x1 {
    size_t width, height;
    int items[1];
};

struct grid_holder {
    struct grid_1x1 a, b;
};

int main(void)
{
    struct grid_holder h = {
        .a = { .width = 1, .height = 1, .items = { 0 } },
        .b = { .width = 1, .height = 1, .items = { 0 } },
    };
    struct grid *a = (struct grid *)&h.a, *b = (struct grid *)&h.b;
}

如果我的所有代码都假定 struct griditems 成员具有 width * height 元素,是否可以按我的方式转换 ab有以上吗?

换句话说,考虑到结构在其他方面相同,具有一个元素的灵活数组成员是否始终与具有一个元素的固定大小数组成员具有相同的偏移量和大小?我想要一个基于 C99 标准的答案。如果偏移量可能不同,是否有其他方法可以实现我在开头所述的目标?

是的,该行为未由 C 标准定义。

C 2018 6.5 7 或 C 1999 6.5 7 中关于哪些类型可用于访问对象的规则不仅仅与对象的布局和表示方式有关。所以问题中的句子“换句话说,如果结构在其他方面相同,那么具有一个元素的灵活数组成员是否总是与具有一个元素的固定大小数组成员具有相同的偏移量和大小?”是不正确的。具有相同的偏移量和大小,即使具有相同的结构定义,也不会使结构兼容别名。

不同的结构是故意不同的类型。考虑这两种类型:

typedef struct { double real, imaginary; } Complex;
typedef struct { double x, y; } Coordinates;

这些结构具有相同的定义(成员名称除外,但即使它们的名称相同,以下内容也成立),但根据 C 标准,它们是不同且不兼容的类型。这意味着在例程中,例如:

double foo(Complex *a, Coordinates *b)
{
    a->real = 3; a->imaginary = 4;
    b->x = 5; b->y = 6;
    return sqrt(a->real*a->real + a->imaginary*a->imaginary);
}

允许编译器在 b->x = 5; b->y = 6; 不能更改 a 的基础上将最后一个语句优化为 return 5; 因为 ab 不能指向同一个对象,或者,如果它们是,则 b->x = 5; b->y = 6; 的行为未定义。

所以关于别名的 C 规则是关于兼容类型以及针对特定情况的各种异常。它们主要不是关于结构的布局。

与上面使用不同但相同定义的结构的示例相比,当我们有多个指向相同结构类型的指针时,编译器不能假设 ab 不是同一对象的别名(不同名称)。在:

double foo(Complex *a, Complex *b)
{
    a->real = 3; a->imaginary = 4;
    b->real = 5; b->imaginary = 6;
    return sqrt(a->real*a->real + a->imaginary*a->imaginary);
}

编译器不能假定 return 值为 5,因为 ab 可能指向同一个对象,在这种情况下 b->real = 5; b->imaginary = 6; 会更改 a.

您需要担心两个不同的问题:

  1. 标准允许实现在结构成员之间放置任意数量的填充,前提是任何结构成员之前的填充总量仅受该成员和前面成员的类型影响。为此,不同大小的数组被认为是不同的类型。至少在理论上,一些针对奇怪架构的实现可能会根据数组的大小改变数组前的填充。例如,在地址标识 32 位字但其中有读写 8 位块的指令的平台上,给定 struct x1 { long l; char a,b[4], c;}; 的实现可以决定填充 b 的开头,因此整个事情都适合一个词,即使给定 struct x1 { long l; char a,b[5], c;}; 的相同实现不会添加这样的填充(因为 b 的部分无论如何都会被分成两个词)。我不知道有任何实际执行此类操作的实现,但委员会可能希望这种松懈唯一重要的时候是在此类平台上开发和使用编译器,在这种情况下,使用此类平台的人会比委员会更能判断不同填充方法的优缺点。

  2. 尽管所有迹象表明通用初始序列规则旨在允许使用指向一种结构类型的指针来检查其他结构类型的通用初始序列的任何部分(这种能力已记录在 1974 C 参考指南中,并且在将联合添加到语言中之后,编译器将不得不竭尽全力支持联合的这种用法,而不用结构指针来支持它),clang 和 gcc 的作者认为已损坏任何依赖这种处理的代码,并主动拒绝支持这种代码,除非使用 -fno-strict-aliasing 标志。

我认为第一个问题纯粹是理论上的,但第二个问题意味着任何试图使用指针访问多个单独声明的结构类型的代码都需要在以下情况下使用 -fno-strict-aliasing 选项使用 gcc 或 clang 构建。这应该不是问题,但第二个问题意味着任何人的代码可能与 clang 或 gcc 一起使用需要确保任何使用这些编译器的人都知道需要 -fno-strict-aliasing(即 "don't be obtuse") 标志。据我所知,专为付费客户设计的编译器即使在使用 -fstrict-aliasing 时也能有效地支持这些结构,因为支持它们很有用而且并不困难,但 gcc 和 clang 的维护者在意识形态上反对这种支持。