【问题标题】:(struct *) vs (void *) -- Funtion prototype equivalence in C11/C99(struct *) vs (void *) -- C11/C99 中的函数原型等价
【发布时间】:2019-07-31 10:32:08
【问题描述】:

昨晚我试图实现GLOB_ALTDIRFUNC,遇到了一个有趣的问题。

虽然在语义上可能略有不同,但(void *)(struct *) 类型是否等效?

示例代码:

typedef struct __dirstream DIR;
struct dirent *readdir(DIR *);
DIR *opendir(const char *);
...
struct dirent *(*gl_readdir)(void *);
void *(*gl_opendir)(const char *);
...
gl_readdir = (struct dirent *(*)(void *))readdir;
gl_opendir = (void *(*)(const char *))opendir;
...
DIR *x = gl_opendir(".");
struct dirent *y = gl_readdir(x);
...

我的直觉是这样说的;它们具有基本相同的存储/表示/对齐要求;并且它们对于参数和返回类型应该是等价的。

c99 standardc11 standard

6.2.5(类型)6.7.6.3(函数声明符(包括原型)) 部分似乎证实了这一点.

所以下面的实现理论上应该是可行的:

现在我在 BSD 和 GNU libc 代码中看到了类似的事情,这很有趣。

这些转换的等价性是编译器实现工件的结果,还是可以从标准规范中推断出的基本限制/属性?

这会导致未定义的行为吗?

@nwellnhof 说:

为了使两种指针类型兼容,两者应相同 限定并且两者都应该是指向兼容类型的指针。

好的,这是关键。 (void *)(struct *)怎么会不兼容?

从 6.3.2.3 指针: 指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应比较 等于原来的指针。如果使用转换后的指针调用 其类型与所指向的类型不兼容的函数, 行为未定义。

尚未确定。

进一步说明:

  • 结构依赖于它们的第一个元素进行对齐,因此结构指针的对齐要求应该与 void 指针相同,对吧?
  • 我最初没有在任何地方指定DIR,但它保证是一个结构。
  • 问题的全部意义在于知道我是否可以避免使用包装器(就像我为 gl_closedir 所做的那样,它的类型显然不兼容)。
  • 虽然 C11/C99 可能不允许这样做,但实际上它被 BSD 和 GNU 系统代码使用,因此可能是其他一些相关标准,例如POSIX,指定行为。

这个特性中的例子:

所以,到目前为止,我的想法是:

  • 我想不出有什么理由不能将 (struct *)(void *) 交换,反之亦然。
  • struct 将具有第一个元素的对齐方式,这可能是 char,因此指针的要求与 void 指针的要求完全相同。
  • 因此,struct 指针应具有与 void 指针相同的实现要求,而所有 struct 指针均等价的要求进一步强化了这一点。
  • 所以(const void *)应该等价于(const struct *)
  • (void *) 应该等同于 (struct *)
  • 等等所有特殊属性。

【问题讨论】:

  • 如果您将某些标签替换为“C”和“language-lawyer”标签,尤其是“C”标签,您可能会得到更好的响应。
  • 您的问题含糊不清,您对什么等价感兴趣,并且某些代码可能隐藏在外部链接后面。请写简单的minimal reproducible example,这样我们就可以准确地看到答案了。
  • @user694733 谢谢,我应该添加更多代码...我认为这很明显,但也许不是。
  • 请按照上一条评论的建议发布 MRE。 void *struct X * 不是兼容的类型。 glibc 是为 gcc 编译而编写的,它不假装可移植或符合标准
  • 我添加了readdiropendir 的声明以澄清问题。如果有错误,请编辑或回滚问题。

标签: c posix function-pointers c11 function-prototypes


【解决方案1】:

仅考虑 ISO C:section 6.3.2.3 指定指针类型之间的哪些强制转换需要不丢失信息:

  • 可以将指向任何对象类型的指针转​​换为指向 void 的指针,然后再返回;结果应与原始指针比较。
  • 指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐引用的类型,则行为未定义。否则,当再次转换回来时,结果将等于原始指针。
  • 指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再转换回来;结果应与原始指针比较。 如果转换后的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

(强调我的)所以,让我们再看看你的代码,添加来自dirent.h 的一些声明:

struct dirent;
typedef /* opaque */ DIR;
extern struct dirent *readdir (DIR *);

struct dirent *(*gl_readdir)(void *);
gl_readdir = (struct dirent *(*)(void *))readdir;
DIR *x = /* ... */;
struct dirent *y = gl_readdir(x);

这会将struct dirent *(*)(DIR *) 类型的函数指针转换为struct dirent *(*)(void *) 类型的函数指针,然后调用转换后的指针。这两种函数指针类型是不兼容的(在大多数情况下,两种类型必须完全相同才能“兼容”;有一堆异常,但在这里都不适用),因此代码具有未定义的行为。

我想强调“它们具有基本相同的存储/表示/对齐要求”不足以避免未定义的行为。臭名昭著的sockaddr mess 涉及具有相同表示和对齐要求的类型,甚至具有相同的初始公共子序列,但struct sockaddrstruct sockaddr_in 仍然是不兼容的类型,并且读取struct sockaddrsa_family 字段认为从 struct sockaddr_in 强制转换仍然是未定义的行为。

在一般情况下,为了避免由于函数指针类型不兼容而导致的未定义行为,您必须编写“粘合”函数,将 void * 转换回底层过程所期望的任何具体类型:

static struct dirent *
gl_readdir_glue (void *closure)
{
    return readdir((DIR *)closure);
}

gl_readdir = gl_readdir_glue;

GLOB_ALTDIRFUNC 是一个 GNU 扩展。 Its specification 显然(对我来说,无论如何)是在没有人担心编译器优化基于永远不会发生未定义行为的假设的时候写的,所以我认为你不应该假设编译器会按你的意思做与gl_readdir = (struct dirent *(*)(void *))readdir; 如果您正在编写使用GLOB_ALTDIRFUNC 的代码,请编写胶水函数。

如果您正在实现 GLOB_ALTDIRFUNC,只需将您从gl_opendir 挂钩中获得的void * 存储在void * 类型的变量中,然后将其直接传递给gl_readdirgl_closedir 钩子。不要试图猜测调用者想要它是什么。


编辑:链接中的代码实际上是glob 的实现。它的作用是通过设置钩子本身将 non-GLOB_ALTDIRFUNC 案例减少为 GLOB_ALTDIRFUNC 案例。而且它没有我推荐的胶水函数,它有gl_readdir = (struct dirent *(*)(void *))readdir; 我不会那样做,但确实这种特殊的未定义行为类不太可能导致编译器和优化级别出现问题通常用于 C 库实现。

【讨论】:

  • 请查看 cmets,我不是在寻求解决方法,而是在询问这种 hack 是否可靠,因为某些标准以某种方式指定了它,即使是晦涩难懂的。
  • @IsmaelLuceno 我相信我是在说这种黑客值得依赖。
  • 好吧,如果我懒惰的话,我会很高兴这是一个微不足道的猜测,但我来这里是为了找出它是否只是一个普遍存在的错误,或者它实际上是可靠的:-P。
  • 你必须承认减少间接性是可取的。
  • @IsmaelLuceno 我诚实的看法是所有这些实现都是错误的,而且我们没有什么可以集体引用你的;我可能说过但其他答案似乎没有涵盖的所有内容。
【解决方案2】:

根据 C99 标准,6.7.5.1 指针声明符:

对于要兼容的两个指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。

所以void *DIR * 不兼容。

来自6.7.5.3 函数声明符(包括原型):

对于要兼容的两种函数类型,两者都应指定兼容的返回类型。此外,参数类型列表(如果两者都存在)应在参数数量和省略号终止符的使用方面达成一致;对应的参数应该有兼容的类型。

所以struct dirent *(*)(void *)gl_readdir 的类型)和struct dirent *(*)(DIR *)readdir 的类型)不兼容。

来自6.3.2.3 指针:

指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应与原始指针比较。如果转换后的指针用于调用类型与指向的类型不兼容的函数,则行为未定义。

所以

gl_readdir = (struct dirent *(*)(void *))readdir;
gl_readdir(x);

是未定义的行为。

【讨论】:

  • 这个答案避免解释为什么,你只是断言它们不兼容,但证据在哪里?证明这比证明它们兼容容易得多,只是一个反例。
  • @IsmaelLuceno 如果第 6.7.5.3 节中引用的文本不够充分,请向您解释究竟什么才是“证明”。在语言律师问题中,这通常是我们寻求的证明。
  • 好吧,基本上你实际上可以实现一个有效的编译器,其中void*struct*不能成功交换......
【解决方案3】:

struct x*struct y* 对于任意两个xy 保证具有相同的表示和对齐要求,union 指针相同,但 void 指针和结构指针不一样:

http://port70.net/~nsz/c/c11/n1570.html#6.2.5p28

指向 void 的指针应具有相同的表示和对齐方式 要求作为指向字符类型的指针。48) 同样,指针 兼容类型的合格或不合格版本应具有 相同的表示和对齐要求。所有指向的指针 结构类型应具有相同的表示和对齐方式 互相要求。所有指向联合类型的指针都应具有 相同的表示和对齐要求。指针 到其他类型不需要具有相同的表示或对齐方式 要求。

此外,函数类型的“子类型”的相同表示和对齐要求是不够的。对于要定义的函数指针调用,函数指针的目标类型必须与实际函数的类型兼容,并且为了函数兼容性,对应的函数参数之间需要严格兼容,这意味着在技术上例如void foo(char*);void foo(char const*); 兼容,即使char*char const* 具有相同的表示和对齐方式。

http://port70.net/~nsz/c/c11/n1570.html#6.7.6.3p15

对于要兼容的两个函数类型,两者都应指定 compatible return types.146) 此外,参数类型列表,如果两者都是 目前,应同意参数的数量和使用 省略号终止符;相应的参数应具有兼容的 类型。如果一种类型有参数类型列表,而另一种类型是 由不属于函数的函数声明符指定 定义并包含一个空标识符列表,参数 列表不应有省略号终止符和每个的类型 参数应与产生的类型兼容 应用默认参数提升。如果一种类型有 参数类型列表,其他类型由函数指定 包含(可能为空的)标识符列表的定义,两者 参数的数量和每个参数的类型应一致 原型参数应与产生的类型兼容 从应用默认参数提升到类型 对应的标识符。 (在确定类型 兼容性和复合类型,每个参数声明 函数或数组类型被视为具有调整后的类型,并且每个 使用限定类型声明的参数被视为具有 其声明类型的非限定版本。)

【讨论】:

    猜你喜欢
    • 2010-12-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-29
    • 2011-12-16
    • 2018-10-27
    • 2017-05-23
    • 2011-02-04
    • 2011-04-28
    相关资源
    最近更新 更多