背景
标准隐含地要求所有指向结构类型(完整、不完整、兼容和不兼容)的指针具有相同的表示和对齐要求的假设始于 C89 - 比标准明确要求它早很多年。其背后的原因是不完整类型在不同翻译单元中的兼容性,尽管根据 C 标准委员会的说法,最初的意图是允许不完整类型与其完整变体兼容,但标准的实际文字并未描述它。这已在 C89 的第二次技术勘误中进行了修改,因此使最初的假设具体化。
兼容性和不完整类型
在阅读与兼容性和不完整类型相关的指南时,感谢 Matt McNabb,我们发现了对原始 C89 假设的进一步了解。
对象和不完整类型的指针派生
C99/C11§6.2.5 p1:
类型分为对象类型、函数类型和不完整类型。
C99/C11§6.2.5 p20:
指针类型可以派生自函数类型、对象类型或不完整类型,称为引用类型。
C99/C11§6.2.5 p22:
未知内容的结构或联合类型是不完整类型。对于该类型的所有声明,通过稍后在相同范围内声明相同的结构或联合标记及其定义内容来完成。
这意味着指针可以派生自对象类型和不完整类型。虽然没有指定不完整的类型不需要完成;过去委员会对此事的回应,并表示没有禁令就足够了,不需要正面声明。
以下指向不完整 'struct never_completed' 的指针永远不会完成:
int main(void)
{
struct never_completed *p;
p = malloc(1024);
}
[Complete code sample]
独立翻译单元的兼容类型
C99/C11§6.7.2.3 p4:
所有具有相同作用域并使用相同标记的结构、联合或枚举类型的声明都声明了相同的类型。
C99/C11§6.2.7 p1:
如果它们的类型相同,则两种类型具有兼容的类型。如果它们的标签(是)相同的标签,则在单独的翻译单元中声明的两种结构类型是兼容的。 [修剪报价] [...]
这一段意义重大,请允许我总结一下:在不同的翻译单元中声明的两种结构类型如果使用相同的标签,则它们是兼容的。如果它们都已完成 - 它们的成员必须相同(根据指定的准则)。
指针的兼容性
C99 §6.7.5.1 p2/C11 §6.7.6.1 p2:
对于要兼容的两个指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。
如果标准要求两个结构在特定条件下,无论是不完整的还是完整的,都必须在不同的翻译单元中兼容,这意味着从这些结构派生的指针也是兼容的。
C99/C11§6.2.5 p20:
可以从对象、函数和不完整类型构造任意数量的派生类型
这些构造派生类型的方法可以递归应用。
并且由于指针派生是递归的,它使得从指向兼容结构类型的指针派生的指针相互兼容。
兼容类型的表示
C99 §6.2.5 p27/C11 §6.2.5 p28:
指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求。
C99/C11§6.3 p2:
将操作数值转换为兼容类型不会导致值或表示形式发生变化。
C99/C11§6.2.5 p26:
类型的合格或不合格版本是属于同一类型类别并具有相同表示和对齐要求的不同类型。
这意味着一致的实现不能对从不完整或完整结构类型派生的指针的表示和对齐要求有明确的判断,因为单独的翻译单元可能具有兼容类型,这将具有共享相同的表示和对齐要求,并且要求对同一结构类型的不完整或完整变体应用相同的不同判断。
以下指向 incomplete 'struct complete_incomplete' 的指针:
struct complete_incomplete **p;
与以下指向 complete 'struct complete_incomplete' 的指针的指针兼容并共享相同的表示和对齐要求:
struct complete_incomplete { int i; } **p;
C89 相关
如果我们想知道关于 C89 的前提,那么 93 年 6 月的缺陷报告#059 质疑:
这两个部分都没有明确要求最终必须完成不完整类型,也没有明确允许不完整类型在整个编译单元中保持不完整。由于此功能对于声明真正的不透明数据类型很重要,因此值得澄清。
考虑到在不同编译单元中定义和实现的相互引用结构,使得不透明数据类型的想法成为不完整数据类型的自然扩展。
委员会的回应是:
委员会在起草 C 标准时考虑并认可了不透明数据类型。
兼容性与互换性
我们已经讨论了结构类型指针的递归指针派生的表示和对齐要求方面,现在我们面临一个非规范脚注提到的问题,“可互换性”:
C99 TC3 §6.2.5 p27Footnote 39/C11 §6.2.5 p28Footnote 48:
相同的表示和对齐要求意味着作为函数的参数、函数的返回值和联合成员的可互换性。
标准规定注释、脚注和示例是非规范性的,并且“仅供参考”。
C99 FOREWORD p6/C11 FOREWORD p8:
[...] 本前言、引言、注释、脚注和示例也仅供参考。
不幸的是,这个令人困惑的脚注从未改变过,因为充其量 - 脚注专门针对引用它的直接类型,因此将脚注表述为好像“表示和对齐要求”的属性没有上下文这些特定类型很容易解释为共享表示和对齐的所有类型的一般规则。如果要在没有特定类型的上下文的情况下解释脚注,那么标准的规范性文本显然并不暗示它,即使不需要辩论“可互换”一词的解释。
结构类型指针的兼容性
C99/C11§6.7.2.3 p4:
所有具有相同作用域并使用相同标记的结构、联合或枚举类型的声明都声明了相同的类型。
C99/C11§6.2.7 p1:
如果类型相同,则两种类型具有兼容的类型。
C99 §6.7.5.1 p2/C11 §6.7.6.1 p2:
对于要兼容的两个指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。
这说明了一个显而易见的结论,不同的结构类型确实是不同的类型,并且因为它们不同所以它们是不相容的。因此,指向两种不同且不兼容的类型的两个指针同样不兼容,无论它们的表示和对齐要求如何。
有效类型
C99/C11§6.5 p7:
对象的存储值只能由具有以下类型之一的左值表达式访问:
与对象的有效类型兼容的类型
C99/C11§6.5 p6:
访问其存储值的对象的有效类型是对象的声明类型,如果有的话。
不兼容的指针不能“互换”作为函数的参数,也不能作为函数的返回值。隐式转换和指定的特殊情况是例外,这些类型不属于任何此类例外。即使我们决定为所说的“可互换性”添加一个不切实际的要求,并说需要显式转换才能使其适用,那么访问具有不兼容有效类型的对象的存储值也会破坏有效的类型规则。为了使它成为现实,我们需要一个当前标准没有的新属性。因此,共享相同的表示和对齐要求以及可转换是不够的。
这使我们可以“作为工会成员”互换,尽管他们确实可以作为工会成员互换-它没有特殊意义。
官方解释
1. 第一个“官方”解释属于 C 标准委员会的成员。他对“旨在暗示可互换性”的解释是,它实际上并不暗示存在这种可互换性,而是实际上对此提出了建议。
尽管我希望它成为现实,但我不会考虑从非规范脚注中获取建议的实现,更不用说不合理的模糊脚注,同时与规范准则相矛盾 - 是一个符合执行。这显然会使利用并依赖于这种“建议”的程序成为非严格遵守的程序。
2. 第二个“官方”解释属于 C 标准委员会的成员/贡献者,根据他的解释,脚注没有引入建议,并且因为(规范)文本标准并不暗示它-他认为这是标准中的缺陷。他甚至提出了改变有效类型规则来解决这个问题的建议。
3. 第三个“官方”解释来自缺陷报告#070 of Dec 93`。在 C89 的上下文中,有人询问是否将传递“无符号整数”类型的程序(预期类型“int”作为具有非原型声明符的函数的参数)引入未定义的行为。
在 C89 中有相同的脚注,与函数参数具有相同的隐含互换性,附加到:
C89 §3.1.2.5 p2:
有符号整数类型的非负值范围是对应无符号整数类型的子范围,相同值在每种类型中的表示是相同的。
委员会回应说,他们鼓励实施者允许这种可互换性发挥作用,但由于这不是一项要求,它会使程序成为非严格符合的程序。
以下代码示例不完全符合。 '&s1' 和 'struct generic **' 共享相同的表示和对齐要求,但它们仍然不兼容。根据有效类型规则,我们正在使用不兼容的有效类型(指向“struct generic”的指针)访问对象“s1”的存储值,而其声明的类型(因此有效类型)是指向“struct s1”的指针'。为了克服这个限制,我们可以将指针用作联合的成员,但是这种约定破坏了泛型的目标。
int allocate_struct(void *p,
size_t s)
{
struct generic **p2 = p;
if ((*p2 = malloc(s)) == NULL)
return -1;
return 0;
}
int main(void)
{
struct s1 { int i; } *s1;
if (allocate_struct(&s1, sizeof *s1) != 0)
return EXIT_FAILURE;
}
[Complete code sample]
以下代码示例严格符合,为了克服有效类型和泛型问题,我们利用了:1. 指向 void 的指针,2. 表示和对齐要求所有指向结构的指针,以及 3.“一般”访问指针的字节表示,同时使用 memcpy 复制表示,而不影响其有效类型。
int allocate_struct(void *pv,
size_t s)
{
struct generic *pgs;
if ((pgs = malloc(s)) == NULL)
return -1;
memcpy(pv, &pgs, sizeof pgs);
return 0;
}
int main(void)
{
struct s1 { int i; } *s1;
if (allocate_struct(&s1, sizeof *s1) != 0)
return EXIT_FAILURE;
}
[Complete code sample]
结论
结论是一致的实现必须分别对所有递归派生的指向结构类型的指针具有相同的表示和对齐要求,无论它们是不完整的还是完整的,以及它们是兼容的还是不兼容的。尽管类型兼容还是不兼容很重要,但由于兼容类型的可能性,它们必须共享表示和对齐的基本属性。如果我们可以直接访问共享表示和对齐的指针,那将是首选,但不幸的是,当前的有效类型规则不需要它。