【问题标题】:Is it undefined behavior to cast from a struct with a flexible array member to an otherwise identical one without?从具有灵活数组成员的结构转换为没有其他相同成员的结构是否是未定义的行为?
【发布时间】:2020-05-06 23:50:21
【问题描述】:

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

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 struct c99


    【解决方案1】:

    是的,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);
    }
    

    允许编译器将最后一条语句优化为return 5;,因为b->x = 5; b->y = 6; 不能更改a,因为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);
    }
    

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

    【讨论】:

    • 关于标准含义的任何问题都将围绕“通过”左值表达式访问对象的存储值意味着什么的问题。如果从字面上理解 N1570 6.5p7,则需要程序员跳过障碍来做很多事情,因为赋值表达式和包含初始化器的声明都不是左值表达式。
    • @supercat:赋值表达式有一个左值表达式作为它的左操作数,很明显赋值访问了该表达式所引用的对象。 (“访问”意味着读取或写入。)初始化没有问题,因为声明对象的有效类型是其声明类型,因此初始化不能访问使用其有效类型以外的任何对象定义的对象。
    • @EricPostpischil:如果带有初始化程序的非 VLA 自动对象声明,则当代码进入其范围时,自动对象就会存在,但直到此后不久才会评估初始化表达式,这意味着对象在其生命周期内被修改。显然,该标准并不是要禁止这种用法,但由于只有故意钝化的编译器才会关心此类标准是否要求对此类声明进行有意义的处理,因此标准的作者无需担心它是否确实如此.
    • @EricPostpischil:否则,someStruct.arrayMember[index] = 23; 怎么样?在评估该表达式时,someStruct.arrayMember 的唯一用途是产生一个指针值。
    • @supercat:初始化是否或何时访问对象没有区别,因为正如我之前解释的,类型匹配,因此不可能违反 6.5 7。
    【解决方案2】:

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

    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 标志。

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

    【讨论】:

    • 通用初始顺序规则仅适用于同一联合中的结构。
    • @EricPostpischil:在 1974 C 参考指南所描述的语言中,它适用于通过指针访问的结构,并定义了事情如何与联合一起工作,并结合说联合中的对象具有相同的地址,这意味着该规则对指针仍然有用(就像在 1974 C 中一样),除非编译器竭尽全力阻止它。
    猜你喜欢
    • 1970-01-01
    • 2020-02-10
    • 2020-04-13
    • 2012-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多