"struct inheritance" 如何不违反严格的别名规则?
How does "struct inheritance" not violate the strict aliasing rule?
C () 中的 "struct inheritance technique" 之所以成为可能,是因为 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.
这个问题分为两部分:
一个。在我看来,这种技术打破了严格的别名规则。我错了吗?如果错了,为什么?
乙。假设这种技术确实是合法的。在那种情况下,如果 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;
}
选项 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;
}
选项 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;
}
我相信 C11 中的这句话(ISO/IEC 9899:2011 §6.5 7)应该可以回答你的一些问题(我强调的):
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its
members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
然后可以通过这个回答更多(ISO/IEC 9899:2011 §6.7.2.1 15):
A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
剩下的可以通过这个片段来回答(ISO/IEC 9899:2011 §7.22.3 1):
The order and contiguity of storage allocated by successive calls to the
aligned_alloc
, calloc
, malloc
, and realloc
functions is unspecified. The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object with a fundamental alignment requirement and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).
总结:
一个。你错了。推理见第一和第二引号。
乙。不,这没有什么区别。请参阅第三个引用(也许是第一个)的推理。
是的,结构的第一个元素之前没有任何填充。
其次,当匿名字段的类型是结构或联合的 typedef 时,代码可以使用 typedef 的名称引用该字段。
这是从 GCC 手册中获取的良好做法:
typedef struct {
// some fields
} A;
typedef struct {
A;
// more fields
} B;
typedef struct {
B;
// yet more fields
} C;
B get_B (struct C *c) { return c->B; } /* access B */
A. It seems to me this technique breaks the strict aliasing rule. Am I wrong, and if so, why?
是的,你错了。我会考虑两种情况:
情况 1:C
已完全初始化
就是这样,例如:
C *c = malloc(sizeof(*c));
*c = (C){0}; // or equivalently, "*c = (C){{{0}}}" to satisfy overzealous compilers
在这种情况下,C
的表示的所有字节都已设置,并且包含这些字节的对象的有效类型 是C
.这来自标准的第 6.5/6 段:
If a value is stored into an object having no declared type through an
lvalue having a type that is not a character type, then the type of
the lvalue becomes the effective type of the object for that access
and for subsequent accesses that do not modify the stored value.
但是结构和数组类型是聚合类型,这意味着这种类型的对象在其中包含其他对象。特别是,每个 C
包含一个 B
标识为其成员 base
。因为此时分配的对象实际上是一个 C
,它包含一个实际上是 B
的子对象。引用 B
的左值的一种语法是 c->base
。该表达式的类型是 B
,因此使用它来访问它所引用的 B
符合严格的别名规则。这必须没问题,否则结构(和数组)根本不起作用,无论是否动态分配。*
但是,正如 中所讨论的,(B *)c
保证与 &c->base
相等(在值 和 类型中)。因此 *(B *)c
是引用 B
的另一个左值,它是 *c
的第一个成员。该表达式的语法与我们考虑的先前左值的语法不同无关紧要。它是 B
类型的左值,与 B
类型的对象相关联,因此使用它来访问它所引用的对象是 SAR 允许的情况之一。
None这与静态和自动分配的情况有什么不同
情况 2:C
未完全初始化
可能是这样的:
C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
因此,我们通过 B
类型的左值分配给已分配对象的初始 B
大小的部分,因此该初始部分的有效类型是 B
。分配的 space 此时不包含(有效)类型 C
的对象。如上所述,我们可以通过引用它们的任何可接受类型的左值来访问 B
及其成员,读取或写入。但是如果我们
我们有一个严格的别名冲突
- 尝试整体阅读
*c
(例如 C c2 = *c;
);
- 尝试读取
base
以外的 C
成员(例如 X x = c->another;
);或
- 尝试通过最不相关类型的左值读取分配的对象(例如
Unrelated_but_not_char u = *(Unrelated_but_not_char *) c;
前两种情况在这里很有趣,它们在动态分配的对象方面是有意义的,当被解释为 C
时,没有被完全初始化。自动分配的对象也会出现类似的不完整初始化情况;它们也会产生未定义的行为,但规则不同。
但是请注意,对分配的 space 的任何写入都没有严格的别名冲突,因为任何此类写入都会(重新)分配(至少)区域的有效类型写给.
这就把我们带到了主要的技巧。如果我们这样做会怎样:
C *c = malloc(sizeof(*c));
c->base = (B){0};
?或者这个:
C *c = malloc(sizeof(*c));
c->another = 0;
分配的对象在第一次写入之前没有任何有效类型(特别是,它没有有效类型C
),因此通过[=31写入成员表达式=] 甚至有意义吗?它们定义明确吗?标准的文字可能支持他们不支持的论点,但没有实施采用这种解释,并且没有理由认为任何人都会这样做。
最符合标准和普遍做法的解释是,通过成员访问左值写入构成同时写入成员及其主机聚合,从而设置整个区域的有效类型,即使只写了一个成员的值。当然,这仍然无法读取未写入值的成员——因为它们的值是不确定的,而不是因为 SAR。
剩下这个案例:
C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
B b2 = c->base; // What about this?
也就是说,如果分配的初始区域space的有效类型是B
,我们可以使用基于类型C
的成员访问左值来读取B
区域的储值?同样,基于没有实际的 C
,有人可能会反驳说,但在实践中,没有实现做出这种解释。正在读取的对象的有效类型——分配的space的初始区域——与用于访问的左值类型相同,所以在这个意义上没有违反 SAR。主机 C
完全是假设的,主要是 语法 的问题,而不是语义问题,因为通过替代表达式,同一区域肯定可以被读取为同一类型的对象.
* 但是 SAR 通过规定 "an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)" 是可以访问的类型之一来阻止关于这一点的任何争论。这消除了围绕访问成员也构成访问包含它的任何对象的位置的任何歧义。
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.
这个问题分为两部分:
一个。在我看来,这种技术打破了严格的别名规则。我错了吗?如果错了,为什么?
乙。假设这种技术确实是合法的。在那种情况下,如果 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;
}
选项 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;
}
选项 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;
}
我相信 C11 中的这句话(ISO/IEC 9899:2011 §6.5 7)应该可以回答你的一些问题(我强调的):
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
然后可以通过这个回答更多(ISO/IEC 9899:2011 §6.7.2.1 15):
A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
剩下的可以通过这个片段来回答(ISO/IEC 9899:2011 §7.22.3 1):
The order and contiguity of storage allocated by successive calls to the
aligned_alloc
,calloc
,malloc
, andrealloc
functions is unspecified. The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object with a fundamental alignment requirement and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).
总结:
一个。你错了。推理见第一和第二引号。
乙。不,这没有什么区别。请参阅第三个引用(也许是第一个)的推理。
是的,结构的第一个元素之前没有任何填充。
其次,当匿名字段的类型是结构或联合的 typedef 时,代码可以使用 typedef 的名称引用该字段。 这是从 GCC 手册中获取的良好做法:
typedef struct {
// some fields
} A;
typedef struct {
A;
// more fields
} B;
typedef struct {
B;
// yet more fields
} C;
B get_B (struct C *c) { return c->B; } /* access B */
A. It seems to me this technique breaks the strict aliasing rule. Am I wrong, and if so, why?
是的,你错了。我会考虑两种情况:
情况 1:C
已完全初始化
就是这样,例如:
C *c = malloc(sizeof(*c));
*c = (C){0}; // or equivalently, "*c = (C){{{0}}}" to satisfy overzealous compilers
在这种情况下,C
的表示的所有字节都已设置,并且包含这些字节的对象的有效类型 是C
.这来自标准的第 6.5/6 段:
If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value.
但是结构和数组类型是聚合类型,这意味着这种类型的对象在其中包含其他对象。特别是,每个 C
包含一个 B
标识为其成员 base
。因为此时分配的对象实际上是一个 C
,它包含一个实际上是 B
的子对象。引用 B
的左值的一种语法是 c->base
。该表达式的类型是 B
,因此使用它来访问它所引用的 B
符合严格的别名规则。这必须没问题,否则结构(和数组)根本不起作用,无论是否动态分配。*
但是,正如 (B *)c
保证与 &c->base
相等(在值 和 类型中)。因此 *(B *)c
是引用 B
的另一个左值,它是 *c
的第一个成员。该表达式的语法与我们考虑的先前左值的语法不同无关紧要。它是 B
类型的左值,与 B
类型的对象相关联,因此使用它来访问它所引用的对象是 SAR 允许的情况之一。
None这与静态和自动分配的情况有什么不同
情况 2:C
未完全初始化
可能是这样的:
C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
因此,我们通过 B
类型的左值分配给已分配对象的初始 B
大小的部分,因此该初始部分的有效类型是 B
。分配的 space 此时不包含(有效)类型 C
的对象。如上所述,我们可以通过引用它们的任何可接受类型的左值来访问 B
及其成员,读取或写入。但是如果我们
- 尝试整体阅读
*c
(例如C c2 = *c;
); - 尝试读取
base
以外的C
成员(例如X x = c->another;
);或 - 尝试通过最不相关类型的左值读取分配的对象(例如
Unrelated_but_not_char u = *(Unrelated_but_not_char *) c;
前两种情况在这里很有趣,它们在动态分配的对象方面是有意义的,当被解释为 C
时,没有被完全初始化。自动分配的对象也会出现类似的不完整初始化情况;它们也会产生未定义的行为,但规则不同。
但是请注意,对分配的 space 的任何写入都没有严格的别名冲突,因为任何此类写入都会(重新)分配(至少)区域的有效类型写给.
这就把我们带到了主要的技巧。如果我们这样做会怎样:
C *c = malloc(sizeof(*c));
c->base = (B){0};
?或者这个:
C *c = malloc(sizeof(*c));
c->another = 0;
分配的对象在第一次写入之前没有任何有效类型(特别是,它没有有效类型C
),因此通过[=31写入成员表达式=] 甚至有意义吗?它们定义明确吗?标准的文字可能支持他们不支持的论点,但没有实施采用这种解释,并且没有理由认为任何人都会这样做。
最符合标准和普遍做法的解释是,通过成员访问左值写入构成同时写入成员及其主机聚合,从而设置整个区域的有效类型,即使只写了一个成员的值。当然,这仍然无法读取未写入值的成员——因为它们的值是不确定的,而不是因为 SAR。
剩下这个案例:
C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
B b2 = c->base; // What about this?
也就是说,如果分配的初始区域space的有效类型是B
,我们可以使用基于类型C
的成员访问左值来读取B
区域的储值?同样,基于没有实际的 C
,有人可能会反驳说,但在实践中,没有实现做出这种解释。正在读取的对象的有效类型——分配的space的初始区域——与用于访问的左值类型相同,所以在这个意义上没有违反 SAR。主机 C
完全是假设的,主要是 语法 的问题,而不是语义问题,因为通过替代表达式,同一区域肯定可以被读取为同一类型的对象.
* 但是 SAR 通过规定 "an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)" 是可以访问的类型之一来阻止关于这一点的任何争论。这消除了围绕访问成员也构成访问包含它的任何对象的位置的任何歧义。