将泛型函数指针转换为含void*参数的特定签名是否合规?
原问题
我尝试构建一个generic_processor接口:调用者将对象指针传入处理器的void*参数,并把具体操作函数显式转换为void(*gfp)(void)(泛型函数指针)供generic_processor使用。
各类型的具体操作函数签名几乎一致:接收对应类型的对象指针、可能的其他参数,返回int。
核心疑问点:generic_processor会把gfp参数转换为一种函数指针类型,其签名与具体操作函数几乎一致,仅第一个参数从具体类型指针改为void*。
根据C标准的相关条款:
- 一种函数类型的指针可转换为另一种函数类型的指针,再转换回原类型;
- 转换结果应与原指针相等;
- 若使用转换后的指针调用类型不兼容的函数,行为未定义。
因此核心问题是:此处的“不兼容”具体指什么?将具体类型指针参数替换为void*的函数指针转换是否合规?
示例代码
#include <stdio.h> // types typedef struct { int a; double d; } circle; typedef struct { int b; float f; } square; // ... 10 types,.. different sizeof() // concrete API int open_circle(circle* c) { printf("opening circle: %d: %f\n", c->a, c->d); return c->a; } int open_square(square* s) { printf("opening square: %d: %f\n", s->b, s->f); return s->b; } int send_circle(circle* c, const char* msg) { printf("sending circle: %d: %f: %s\n", c->a, c->d, msg); return -c->a; } int send_square(square* s, const char* msg) { printf("sending square: %d: %f: %s\n", s->b, s->f, msg); return -s->b; } // ten more operations for each type // "genericised" function pointer types (note the void* params!!) typedef int (*open_fpt)(void* o); typedef int (*send_fpt)(void* o, const char*); typedef void (*gfp)(void); // generic function pointer int generic_processor(void* obj, gfp open, gfp send) { int sum = 0; sum += ((open_fpt)open)(obj); sum += ((send_fpt)send)(obj, "generically sent"); return sum; } int main() { circle c = {2, 22.2}; square s = {3, 33.3F}; int net = 0; net += generic_processor(&c, (gfp)open_circle, (gfp)send_circle); net += generic_processor(&s, (gfp)open_square, (gfp)send_square); printf("net %d\n", net); return 0; }
编译运行结果
使用GCC 13.2编译无警告且运行正常:
gcc -std=c99 -g -o gfp tests/gfp.c -Wall -Wextra -pedantic -fsanitize=address,leak,undefined && ./gfp opening circle: 2: 22.200000 sending circle: 2: 22.200000: generically sent opening square: 3: 33.299999 sending square: 3: 33.299999: generically sent net 0
EDIT1
我从讨论中学到了很多,原示例代码并不合规,因为void*与square*不兼容——结构体指针与非结构体指针的表示形式可能不同。
不过标准保证所有结构体指针的大小和表示形式一致,因此只需将void*改为虚拟结构体类型obj_*,代码即可符合标准,完整修正代码见我的补充回答。
EDIT2
关于函数指针转换时void*或struct*的使用,C标准定义并不完全清晰,甚至不同部分存在一定矛盾。
这种模糊性导致像OpenSSL这类大型安全项目使用了与本文类似的技术,却在LLVM发布的Clang 17(2023版)的UBSAN中被判定为未定义行为(UB)并抛出运行时警告。
鉴于这类基础问题缺乏明确标准,我决定采用“宏thunking黑魔法”方案,详见我的另一篇补充回答。
内容的提问来源于Stack Exchange,提问作者Oliver Schönrock
相关产品推荐
相关产品推荐

