昨晚我试图实现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标准和c11标准的第6.2.5节(类型)和6.7.6.3(函数声明符(包括原型))似乎证实了这一点。
因此,以下实现在理论上应该有效:
- https://www.openwall.com/lists/musl/2019/07/30/8
现在我看到在BSD和GNU libc代码中做了类似的事情,这很有趣。
这些转换的等效性是编译器的实现工件的结果,还是可以从标准规范中推断出的基本限制/属性?
这是否会导致未定义的行为?
@nwellnhof 说:
要使两种指针类型兼容,两者应相同 限定,并且两者都应是指向兼容类型的指针。
好的,这是关键。(void *)
和(struct *)
怎么会不兼容?
从 6.3.2.3 指针: 指向一种类型的函数的指针可以转换为指向另一种类型函数的指针,然后再返回;结果应比较 等于原始指针。如果使用转换后的指针调用 类型与指向类型不兼容的函数, 行为未定义。
尚未确定。
进一步澄清:
结构- 依赖于它们的第一个元素进行对齐,因此结构指针的对齐要求应该与空指针的对齐要求相同,对吗?
- 我最初没有在任何地方指定
DIR
,但它保证是一个结构。 - 问题的重点在于知道我是否可以避免使用包装器(就像我为 gl_closedir 所做的那样,其类型显然不兼容)。
- 虽然 C11/C99 可能不允许这样做,但实际上 BSD 和 GNU 系统代码正在使用它,所以也许其他一些相关标准,例如 POSIX,指定了行为。
在野外的例子,在这个功能中:
- OpenBSD glob.c
- glibc glob.h (ABI 不应该改变,由
_GNU_SOURCE
->__USE_GNU
控制)。
所以,到目前为止,我的想法是:
- 我想不出有什么理由你不能用
(void *)
交换(struct *)
,反之亦然。 struct
将具有第一个元素的对齐方式,这可能是一个char
因此指针的要求与指针的要求完全相同void
。- 因此,
struct
指针应具有与void
指针相同的实现要求,所有struct
指针等效的要求进一步强化了这一点。 - 所以
(const void *)
应该等同于(const struct *)
(void *)
应该等同于(struct *)
- 等等,具有所有特殊属性。
仅考虑 ISO C:第 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
混乱涉及具有相同表示和对齐要求的类型,甚至具有相同的初始公共子序列,但struct sockaddr
和struct sockaddr_in
仍然不是兼容的类型,并且读取从struct sockaddr_in
转换的struct sockaddr
的sa_family
字段仍然是未定义的行为。
在一般情况下,为了避免由于不兼容的函数指针类型而导致未定义的行为,您必须编写"glue"函数,这些函数从void *
转换回基础过程所需的任何具体类型:
static struct dirent *
gl_readdir_glue (void *closure)
{
return readdir((DIR *)closure);
}
gl_readdir = gl_readdir_glue;
GLOB_ALTDIRFUNC
是一个GNU扩展。 它的规范显然(对我来说,无论如何)是在没有人担心编译器基于未定义行为永远不会发生的假设进行优化的日子里写的,所以我认为你不应该假设编译器会按照你的意思做gl_readdir = (struct dirent *(*)(void *))readdir;
如果你正在编写使用GLOB_ALTDIRFUNC
的代码, 编写粘附函数。
如果你正在实现GLOB_ALTDIRFUNC
,只需将从gl_opendir
钩子获得的void *
存储在类型为void *
的变量中,并将其直接传递给gl_readdir
和gl_closedir
钩子。 不要试图猜测调用者想要它是什么。
编辑:链接中的代码实际上是glob
的实现。 它的作用是通过设置钩子本身将非GLOB_ALTDIRFUNC
的情况减少到GLOB_ALTDIRFUNC
的情况。 而且它没有我推荐的粘附函数,它gl_readdir = (struct dirent *(*)(void *))readdir;
我不会那样做,但确实,这种特定的未定义行为不太可能导致通常用于 C 库实现的编译器和优化级别出现问题。
从 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);
是未定义的行为。
任何两个x
和y
的struct x*
和struct y*
保证具有相同的表示和对齐要求,对于union
指针相同,但无效指针和结构指针则不相同:
http://port70.net/~nsz/c/c11/n1570.html#6.2.5p28
指向 void 的指针应具有相同的表示和对齐方式 要求作为指向字符类型的指针。48) 同样,指针 对合格或非合格版本的兼容类型应具有 相同的表示和对齐要求。所有指向的指针 结构类型应具有相同的表示和对齐方式 要求彼此。所有指向联合类型的指针都应具有 彼此相同的表示和对齐要求。指针 到其他类型不需要具有相同的表示或对齐方式 要求。
此外,函数类型的"子类型"的相同表示和对齐要求是不够的。对于要定义的通过函数指针进行的调用,函数指针的目标类型必须与实际函数的类型兼容,并且为了函数兼容性,需要相应函数参数之间的严格兼容性,这意味着技术上例如,即使char*
和char const*
具有相同的表示和对齐方式,void foo(char*);
也不兼容void foo(char const*);
。
http://port70.net/~nsz/c/c11/n1570.html#6.7.6.3p15
对于要兼容的两种函数类型,两者都应指定兼容 返回类型.146) 此外,参数类型列表,如果两者都是 目前,应在参数数量和使用中达成一致 省略号终止符;相应的参数应具有兼容 类型。如果一种类型具有参数类型列表,而另一种类型是 由不属于函数的函数声明符指定 定义,并且包含一个空标识符列表,即参数 列表不得有省略号终止符和每个终止符的类型 参数应与由 应用默认参数升级。如果一种类型具有 参数类型列表,其他类型由函数指定 包含(可能为空的)标识符列表的定义,两者 应同意参数的数量和每个参数的类型 原型参数应与结果类型兼容 从默认参数升级的应用到 相应的标识符。(在确定类型 兼容性和复合类型,每个参数声明 函数或数组类型被视为具有调整的类型,并且每个 使用限定类型声明的参数被视为具有 其声明类型的非限定版本。