【问题标题】:Are all pointers derived from pointers to structure types the same?从指向结构类型的指针派生的所有指针都相同吗?
【发布时间】:2014-08-09 20:45:49
【问题描述】:

问题

从指向结构类型的指针派生的所有指针是否都相同的问题并不容易回答。我认为这是一个重要的问题,主要有以下两个原因。

A.缺少指向“任何”不完整或对象类型的指针,对方便的函数接口施加了限制,例如:

int allocate(ANY_TYPE  **p,
             size_t    s);

int main(void)
{
    int *p;
    int r = allocate(&p, sizeof *p);
}

[Complete code sample]

指向“任何”不完整或对象类型的现有指针明确描述为:

C99/C11§6.3.2.3 p1:

指向 void 的指针可以转换为指向任何不完整或对象类型的指针或从指针转换。 [...]

从现有的指向“任何”不完整或对象类型的指针派生的指针,指向 void 的指针,严格来说是指向 void 的指针,并且不需要与派生自指向 ' 的指针的指针转换任何'不完整或对象类型。


B. 程序员利用与指针泛化相关的非必需假设的约定并不罕见,有意或无意,同时取决于他们的经验具体实现。假设,例如可转换、可表示为整数或共享一个公共属性:对象大小、表示或对齐方式。


标准的话

根据C99 §6.2.5 p27/C11 §6.2.5 p28

[...] 所有指向结构类型的指针都应具有相同的表示和对齐要求。 [...]

后跟C99 TC3 Footnote 39/C11 Footnote 48

相同的表示和对齐要求意味着作为函数的参数、函数的返回值和联合成员的可互换性。

虽然标准没有说:“指向结构类型的指针”和选择了以下词语:“指向结构类型的所有指针”,但它没有明确指定它是否适用于此类的递归推导指针。在标准中提到指针的特殊属性的其他情况下,它没有明确指定或提及递归指针派生,这意味着“类型派生”适用,或者不适用,但没有明确提及。

虽然在引用类型时使用“所有指针”这一短语只使用了两次,(用于结构和联合类型),而不是更明确的短语:“指向”在整个标准中使用,我们无法断定它是否适用于此类指针的递归派生。

【问题讨论】:

  • 无论您的主要问题的答案是什么,allocate 无论如何都不是一个方便的界面。
  • 此外,您的示例似乎与该问题无关。如果它在声明中有TYPE1,在调用站点有TYPE2,它可以被认为是指向具有相同表示的不同结构的指针的合法示例。但就目前而言,您的示例只有指向 same 结构类型的指针,以及指向这些指针的指针。
  • @WouterHuysentruit 我更喜欢能解释一切的答案。如果这变得很长,## tl;dr 部分就有意义了。但请记住,关于 SO 的两个问题和答案也应该是其他人的榜样,并帮助有类似(即使不完全相同)问题的人。
  • 很公平,为努力点赞。继续努力:)
  • @n.m.如果这行得通,那将是realloc 的一种比当前更好的形式(就地重新分配指针并使用返回值来检查它是否成功) - 目前编写从 realloc 恢复的代码有点麻烦失败

标签: c pointers struct c99 strict-aliasing


【解决方案1】:

背景

标准隐含地要求所有指向结构类型(完整、不完整、兼容和不兼容)的指针具有相同的表示和对齐要求的假设始于 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]


结论

结论是一致的实现必须分别对所有递归派生的指向结构类型的指针具有相同的表示和对齐要求,无论它们是不完整的还是完整的,以及它们是兼容的还是不兼容的。尽管类型兼容还是不兼容很重要,但由于兼容类型的可能性,它们必须共享表示和对齐的基本属性。如果我们可以直接访问共享表示和对齐的指针,那将是首选,但不幸的是,当前的有效类型规则不需要它。

【讨论】:

  • 就像我们有整数数组、指针数组等一样,我们也可以有结构变量数组。为了有效地使用结构变量数组,我们使用结构类型的指针。我们也可以有指向单个结构变量的指针,但它主要用于结构变量数组。
  • K&R 定义的语言使得函数可以接受指向结构的指针数组并对数组元素进行操作,而不必担心它们识别出什么样的结构。将数组传递给这样的函数时需要进行强制转换,否则事情会起作用。 C89 没有要求实现继续支持这种能力,但由于这种能力是有用的,因此实现支持它,直到有人决定通过删除虽然有用但没有用的特性使语言“更好”标准规定的。
  • 如果目标在分配的存储空间中,使用memcpy) 的allocate_struct 版本是否可以保证工作?根据我对标准的阅读,在memcpy 之后,持有相关指针的存储的有效类型将是struct generic*
【解决方案2】:

我的回答是“不”。

在我所知道的任何 C 标准中,没有任何措辞暗示了其他情况。所有指向结构类型的指针都具有相同的表示和对齐要求这一事实与任何派生类型无关。

这是完全有道理的,任何其他现实似乎都不一致。考虑替代方案:

让我们将指向结构类型“A”的指针的对齐和表示要求称为“A”。假设任何“递归派生类型”共享需求“A”。

让我们将指向联合类型的指针的对齐和表示要求称为“B”。假设任何“递归派生类型”共享需求“B”。

假设“A”和“B”不相同[1]。此外,让我们假设它们不能同时满足。 (例如 4 字节表示和 8 字节表示。)

现在从两者派生一个类型:

  1. 要求为“A”的类型
  2. 具有“B”要求的类型

现在你有一个类型的要求是不可能满足的,因为它必须满足“A”和“B”,但它们不能同时满足。

也许您认为派生类型具有一直到单个祖先的平坦血统,但事实并非如此。派生类型可以有许多祖先。 “派生类型”的标准定义对此进行了讨论。

[1] 虽然这可能看起来不合理、不太可能和愚蠢,但这是允许的。

【讨论】:

  • “假设任何“递归派生类型”共享需求“A”。 - 你假设的理由是什么?符合要求的实现可能具有与 struct X * 不同大小/代表的 struct X **
  • M. M:请重新考虑一下“假设”这个词是什么意思。思想练习的目的是通过证明其不一致来反对这种要求的可能性。我们在同一个团队中。
猜你喜欢
  • 1970-01-01
  • 2013-12-13
  • 2016-02-19
  • 1970-01-01
  • 2019-03-18
  • 2018-07-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多