“结构继承”如何不违反严格的别名规则?

Avi*_*ohn 6 c standards struct

C 中的“结构继承技术”(如本问题所述)是由于以下事实而成为可能的:C 标准保证结构的第一个成员在它之前永远不会有任何填充(?),并且结构的地址第一个成员始终等于结构本身的地址。

这允许如下使用:

typedef struct {
    // some fields
} A;

typedef struct {
    A base;
    // more fields
} B;

typedef struct {
    B base;
    // yet more fields
} C;

C* c = malloc(sizeof(C));
// ... init c or whatever ...
A* a = (A*) c;
// ... access stuff on a etc.
B* b = (B*) c;
// ... access stuff on b etc.
Run Code Online (Sandbox Code Playgroud)

这个问题有两个部分:

答:在我看来,这种技术打破了严格的别名规则。我错了吗?如果错了,为什么?

B. 假设这种技术确实合法。在这种情况下,如果A:我们首先将对象存储在其特定类型的左值中,然后将其向下或向上转换为不同的类型,或者B:如果我们将其直接转换为所需的特定类型,这会有所不同吗?此刻,而不首先将其存储在特定类型的左值中?

例如,这三个选项都同样合法吗?

选项1:

C* make_c(void) {
    return malloc(sizeof(C));
}    

int main(void) {
    C* c = make_c(); // First store in a lvalue of the specific type
    A* a = (A*) c;
    // ... do stuff with a
    C* c2 = (C*) a; // Cast back to C
    // ... do stuff with c2

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

选项2:

C* make_c(void) {
    return malloc(sizeof(C));
}    

int main(void) {
    A* a = (A*) make_c(); // Don't store in an lvalue of the specific type, cast right away
    // ... do stuff with a
    C* c2 = (C*) a; // Cast back to C
    // ... do stuff with c2

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

选项 3:

int main(void) {
    A* a = (A*) malloc(sizeof(C)); // Don't store in an lvalue of the specific type, cast right away
    // ... do stuff with a
    C* c2 = (C*) a; // Cast to C - even though the object was never actually stored in a C* lvalue
    // ... do stuff with c2

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

Joh*_*ger 5

答:在我看来,这种技术打破了严格的别名规则。我错了吗?如果错了,为什么?

是的,你错了。我会考虑两种情况:

情况1:C完全初始化

就是这样,例如:

C *c = malloc(sizeof(*c));
*c = (C){0};  // or equivalently, "*c = (C){{{0}}}" to satisfy overzealous compilers
Run Code Online (Sandbox Code Playgroud)

在这种情况下,a 表示的所有字节都C被设置,并且包含这些字节的对象的有效类型C是。这来自标准的第 6.5/6 段:

如果通过具有非字符类型类型的左值将值存储到没有声明类型的对象中,则该左值的类型将成为该对象的有效类型,用于该访问以及后续不修改该值的访问。储值。

但结构体和数组类型是聚合类型,这意味着此类类型的对象中包含其他对象。特别是,每个都C包含一个B标识为其成员base。因为此时分配的对象实际上是 a C,所以它包含一个实际上是 a 的子对象B。引用该值的左值的一种语法Bc->base。该表达式的类型是B,因此使用它来访问B它所引用的 与严格别名规则是一致的。这必须没问题,否则结构(和数组)根本无法工作,无论是否动态分配。*

但是,正如我在回答您之前的问题时所讨论的,(B *)c保证等于(在值类型上)等于&c->base。因此*(B *)c,另一个左值引用的B是 的第一个成员*c。该表达式的语法与我们之前考虑的左值的语法不同并不重要。它是一个类型为 的左值B,与类型为 的对象关联B,因此使用它来访问它所引用的对象是 SAR 允许的情况之一。

这与静态和自动分配的情况没有任何不同。

情况2:C未完全初始化

可能是这样的:

C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
Run Code Online (Sandbox Code Playgroud)

因此,我们B通过类型为 的左值分配给已分配对象的初始大小部分B,因此该初始部分的有效类型为B。此时分配的空间不包含(有效)类型的对象C。我们可以B通过任何引用它们的可接受类型的左值来访问 及其成员,读取或写入,如上所述。但是如果我们

  • *c尝试整体阅读(例如 C c2 = *c;);
  • 尝试读取除(例如C)以外的成员;或者base X x = c->another;
  • 尝试通过大多数不相关类型的左值读取分配的对象(例如 Unrelated_but_not_char u = *(Unrelated_but_not_char *) c;

这里对前两种情况感兴趣,当解释为 a 时,它们就动态分配的对象而言是有意义的C,未完全初始化。自动分配的对象也可能出现类似的不完整初始化情况;它们也会产生未定义的行为,但规则不同。

但请注意,对已分配空间的任何写入都不存在严格的别名冲突,因为任何此类写入都会(重新)分配(至少)写入区域的有效类型。

这给我们带来了主要的棘手之处。如果我们这样做怎么办:

C *c = malloc(sizeof(*c));
c->base = (B){0};
Run Code Online (Sandbox Code Playgroud)

?或这个:

C *c = malloc(sizeof(*c));
c->another = 0;
Run Code Online (Sandbox Code Playgroud)

分配的对象在第一次写入之前没有任何有效类型(特别是它没有有效类型C),因此通过 Even 写入成员表达式*c有意义吗?它们定义明确吗?标准的文字可能支持他们不支持的论点,但没有任何实现采用这种解释,并且没有理由认为任何实现都会采用这种解释。

与标准字面意义和普遍实践最一致的解释是,通过成员访问左值进行写入构成同时写入成员及其主机聚合,从而设置整个区域的有效类型,即使只有一个成员的值被写入。当然,这仍然不意味着可以读取尚未写入值的成员——因为它们的值是不确定的,而不是因为 SAR。

剩下这个案子:

C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
B b2 = c->base;            // What about this?
Run Code Online (Sandbox Code Playgroud)

也就是说,如果分配空间的初始区域的有效类型是B,我们可以使用基于类型的成员访问左值C来读取该区域的存储值B吗?同样,有人可能会争论不存在,因为没有实际的C,但在实践中,没有实现做出这种解释。正在读取的对象的有效类型(分配空间的初始区域)与用于访问的左值的类型相同,因此从这个意义上讲,不存在 SAR 违规。宿主完全是假设的,这主要是语法C问题,而不是语义问题,因为同一区域肯定可以通过替代表达式读取为同一类型的对象。


*但 SAR 仍然阻止了关于这一点的任何争论,规定“在其成员中包括上述类型之一的聚合或联合类型(递归地包括子聚合或包含联合的成员)”是以下类型之一:可以访问。这消除了访问成员也构成访问包含该成员的任何对象这一位置的任何歧义。