所有指针都是从指向结构类型的指针派生的吗?

Dro*_* K. 28 c struct pointers c99 strict-aliasing

问题

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

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

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

int main(void)
{
    int *p;
    int r = allocate(&p, sizeof *p);
}
Run Code Online (Sandbox Code Playgroud)

[ 完整代码示例 ]

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

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:

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

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

虽然在引用类型时使用"所有指针"的措辞使用两次,(对于结构和联合类型),而不是更明确的措辞:在整个标准中使用的"指针",我们不能总结它是否适用于这种指针的递归推导.

Dro*_* K. 29

背景

假设标准隐含地要求所有指向结构类型的指针(完整,不完整,兼容和不兼容),具有相同的表示和对齐要求,从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);
}
Run Code Online (Sandbox Code Playgroud)

[ 完整代码示例 ]

兼容类型的单独翻译单元

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:

类型的限定或非限定版本是属于相同类型类别且具有相同表示和对齐要求的不同类型.

这意味着符合实现不能对从不完整或完整结构类型派生的指针的表示和对齐要求有明确的判断,因为单独的翻译单元可能具有兼容类型,这将需要共享相同的表示和对齐要求,并且需要对相同结构类型的不完整或完整变体应用相同的不同判断.

以下指针指向不完整的 'struct complete_incomplete':

struct complete_incomplete**p;

兼容并共享相同的表示和对齐要求,如下面指向指向完成 'struct complete_incomplete'的指针:

struct complete_incomplete {int i; }**p;


C89相关

如果我们想知道关于C89的前提#059,那么Jun 93'的缺陷报告就提出质疑:

这两个部分都没有明确要求最终必须完成不完整的类型,也不明确允许不完整的类型对整个编译单元保持不完整.由于此功能对于声明真正的不透明数据类型非常重要,因此值得澄清.

考虑在不同编译单元中定义和实现的相互参照结构使得不透明数据类型的概念成为不完整数据类型的自然扩展.

委员会的回应是:

委员会在起草C标准时考虑并批准了不透明的数据类型.


兼容性与可互换性

我们已经介绍了有关结构类型指针的递归指针派生的表示和对齐要求的方面,现在我们面临一个非规范性脚注提到的问题,"可互换性":

C99 TC3 §6.2.5 p27 Footnote 39/ C11 §6.2.5 p28 Footnote 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十二月93`的.在C89的上下文中,有人询问是否有一个程序传递'unsigned int'类型,其中类型为'int',作为具有非原型声明符的函数的参数,以引入未定义的行为.

在C89中有相同的脚注,与函数的参数具有相同的隐含互换性,附加到:

C89 §3.1.2.5 p2:

有符号整数类型的非负值范围是相应无符号整数类型的子范围,并且每种类型中相同值的表示相同.

该委员会回应称,他们鼓励实施者允许这种互换性,但由于这不是一项要求,因此它使该程序成为非严格符合要求的程序.


以下代码示例不严格符合.'&s1'和'struct generic**'共享相同的表示和对齐要求,但它们是不兼容的.根据有效类型规则,我们使用不兼容的有效类型访问对象's1'的存储值,指向'struct generic',而它的声明类型,因此有效类型,是指向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;
}
Run Code Online (Sandbox Code Playgroud)

[ 完整代码示例 ]


下面的代码示例是严格符合的,为了克服有效类型和通用的问题,我们利用: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;
}
Run Code Online (Sandbox Code Playgroud)

[ 完整代码示例 ]


结论

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