(struct *) vs (void *) -- C11/C99 中的函数原型等价

(struct *) vs (void *) -- Funtion prototype equivalence in C11/C99

昨晚我试图实现 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);
...

我的直觉是这样的;他们有基本相同的 storage/representation/alignment 要求;它们对于参数和 return 类型应该是等价的。

6.2.5(类型)6.7.6.3(函数声明符(包括原型)) c99 standard and the c11 standard 似乎证实了这一点。

所以理论上应该可以实现以下实现:

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

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

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

@nwellnhof 说:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

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

From 6.3.2.3 Pointers: A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

尚未确定。

进一步说明:

在野外的例子,在这个特性中:

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

来自 C99 标准,6.7.5.1 指针声明符:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

所以 void *DIR * 不兼容。

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

For two function types to be compatible, both shall specify compatible return types. Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types.

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

来自 6.3.2.3 指针:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

所以

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

是未定义的行为。

struct x*struct y* 对于任何两个 xy 保证具有相同的表示和对齐要求,对于 union 指针也是如此,但不是空指针和结构指针:

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

A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.48) Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.

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

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

For two function types to be compatible, both shall specify compatible return types.146) Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. If one type has a parameter type list and the other type is specified by a function definition that contains a (possibly empty) identifier list, both shall agree in the number of parameters, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier. (In the determination of type compatibility and of a composite type, each parameter declared with function or array type is taken as having the adjusted type and each parameter declared with qualified type is taken as having the unqualified version of its declared type.)

单独考虑 ISO C:section 6.3.2.3 指定指针类型中的哪些转换需要不丢失信息:

  • A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
  • A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.
  • A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.

(强调我的)所以,让我们再次查看您的代码,添加来自 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 *) 的函数指针,然后调用转换后的指针。这两种函数指针类型不兼容(在大多数情况下,两种类型必须完全相同才能成为 "compatible";有很多例外,但其中 none 适用于此)所以代码未定义行为。

我想强调 "they have basically the same storage/representation/alignment requirements" 不足以避免未定义的行为。臭名昭著的 涉及具有相同表示和对齐要求的类型,甚至相同的初始公共子序列,但 struct sockaddrstruct sockaddr_in 仍然是不兼容的类型,阅读 sa_familystruct sockaddr_in 转换而来的 struct sockaddr 的字段仍然是未定义的行为。

在一般情况下,为了避免由于不兼容的函数指针类型导致的未定义行为,您必须编写 "glue" 函数,将 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 * 存储在类型为 [= 的变量中21=],并将其直接传递给 gl_readdirgl_closedir 挂钩。不要试图猜测来电者想要它是什么。


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