假设我有一个接受void (*)(void*)
函数指针的函数用作回调函数:
void do_stuff(void (*callback_fp)(void*), void* callback_arg);
现在,如果我有这样的功能:
void my_callback_function(struct my_struct* arg);
我可以安全地这样做吗?
do_stuff((void (*)(void*)) &my_callback_function, NULL);
我已经看过这个问题并且我已经看过一些C标准,它们说你可以转换为'兼容函数指针',但我找不到'兼容函数指针'的含义.
就C标准而言,如果将函数指针强制转换为不同类型的函数指针然后调用它,则它是未定义的行为.见附件J.2(资料性附录):
在以下情况下,行为未定义:
指针用于调用类型与指向类型不兼容的函数(6.3.2.3).
第6.3.2.3节第8段内容如下:
指向一种类型的函数的指针可以被转换为指向另一种类型的函数的指针并且再次返回; 结果应该等于原始指针.如果转换的指针用于调用类型与指向类型不兼容的函数,则行为未定义.
换句话说,您可以将函数指针强制转换为不同的函数指针类型,再将其强制转换并调用它,事情就可以了.
兼容的定义有点复杂.它可以在第6.7.5.3节第15段中找到:
要使两种功能类型兼容,两者都应指定兼容的返回类型127.
此外,参数类型列表(如果两者都存在)应在参数的数量和省略号终止符的使用中一致; 相应的参数应具有兼容的类型.如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分并且包含空标识符列表,则参数列表不应具有省略号终止符,并且每个参数的类型应为与应用默认参数促销产生的类型兼容.如果一个类型具有参数类型列表而另一个类型由包含(可能为空)标识符列表的函数定义指定,则两者都应在参数数量上一致,并且每个原型参数的类型应与将默认参数提升应用于相应标识符类型所产生的类型兼容.(在确定类型兼容性和复合类型时,使用函数或数组类型声明的每个参数都被视为具有调整类型,并且使用限定类型声明的每个参数都被视为具有其声明类型的非限定版本.)
127)如果两种函数类型都是"旧样式",则不比较参数类型.
为确定是否两种类型的规则在6.2.7节中描述兼容的,我不会在这里引用他们,因为他们是相当长的,但你可以在阅读C99标准(PDF)的草案.
这里的相关规则见第6.7.5.1节第2段:
要使两个指针类型兼容,两者都应具有相同的限定条件,并且两者都应是兼容类型的指针.
因此,由于void*
是不兼容的struct my_struct*
,类型的函数指针void (*)(void*)
不符合类型的函数指针兼容void (*)(struct my_struct*)
,所以这个转换函数指针是技术上未定义的行为.
但实际上,在某些情况下,您可以安全地使用转换函数指针.在x86调用约定中,参数被推送到堆栈上,并且所有指针都是相同的大小(x86中为4个字节,x86_64中为8个字节).调用函数指针归结为推送堆栈上的参数并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念.
你绝对不能做的事情:
在不同调用约定的函数指针之间转换.你会搞砸堆栈,最糟糕的是,在最糟糕的情况下,崩溃会在一个巨大的安全漏洞中成功.在Windows编程中,您经常传递函数指针.Win32的期望所有的回调函数使用stdcall
调用约定(其中宏CALLBACK
,PASCAL
和WINAPI
所有扩展到).如果传递使用标准C调用约定(cdecl
)的函数指针,则会导致错误.
在C++中,在类成员函数指针和常规函数指针之间进行转换.这通常会让C++新手兴奋不已.类成员函数具有隐藏this
参数,如果将成员函数强制转换为常规函数,则无法this
使用对象,同样会导致很多错误.
另一个可能有效的坏主意也是未定义的行为:
在函数指针和常规指针之间进行转换(例如,将a转换void (*)(void)
为a void*
).函数指针的大小不一定与常规指针相同,因为在某些体系结构中它们可能包含额外的上下文信息.这可能在x86上运行正常,但请记住它是未定义的行为.
我最近询问了有关GLib中某些代码的相同问题.(GLib是GNOME项目的核心库,用C语言编写.)我被告知整个slot'n'signals框架依赖于它.
在整个代码中,有许多从类型(1)到(2)的转换实例:
typedef int (*CompareFunc) (const void *a,
const void *b)
typedef int (*CompareDataFunc) (const void *b,
const void *b,
void *user_data)
链接通常是这样的调用:
int stuff_equal (GStuff *a, GStuff *b, CompareFunc compare_func) { return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL); } int stuff_equal_with_data (GStuff *a, GStuff *b, CompareDataFunc compare_func, void *user_data) { int result; /* do some work here */ result = compare_func (data1, data2, user_data); return result; }
请访问g_array_sort()
:http://git.gnome.org/browse/glib/tree/glib/garray.c
上面的答案是详细的,可能是正确的 - 如果你坐在标准委员会.亚当和约翰尼斯因其精心研究的回应值得赞扬.但是,在野外,你会发现这段代码工作得很好.争议?是.考虑一下:GLib使用各种编译器/链接器/内核加载器(GCC/CLang/MSVC)在大量平台(Linux/Solaris/Windows/OS X)上编译/工作/测试.我想,标准应该被诅咒.
我花了一些时间思考这些答案.这是我的结论:
如果您正在编写回调库,这可能没问题.注意事项 - 使用风险自负.
否则,不要这样做.
在编写此响应后深入思考,如果C编译器的代码使用同样的技巧,我不会感到惊讶.既然(大多数/全部?)现代C编译器都是自举的,这意味着这个技巧是安全的.
研究一个更重要的问题:有人能找到一个平台/编译器/连接/加载其中这一招并不能正常工作?主要的布朗尼指向那个.我敢打赌,有些嵌入式处理器/系统不喜欢它.但是,对于桌面计算(可能还有手机/平板电脑),这个技巧可能仍然有效.
重点不在于你是否可以.琐碎的解决方案是
void my_callback_function(struct my_struct* arg); void my_callback_helper(void* pv) { my_callback_function((struct my_struct*)pv); } do_stuff(&my_callback_helper);
如果确实需要,一个好的编译器只会为my_callback_helper生成代码,在这种情况下你会很高兴它.
如果返回类型和参数类型兼容,则您具有兼容的函数类型-基本上(实际上更为复杂:)。兼容性与“相同类型”相同,只是宽松程度不同,以允许具有不同类型,但仍具有某种形式表示“这些类型几乎相同”。例如,在C89中,如果两个结构在其他方面相同,但它们的名称不同,则它们是兼容的。C99似乎已经改变了这一点。引用c基本原理文档(强烈建议阅读,顺便说一句!):
即使两个声明的文本来自同一个include文件,但两个不同翻译单元中的结构,联合或枚举类型声明也不会正式声明同一类型,因为翻译单元本身是不相交的。因此,标准为此类类型指定了其他兼容性规则,因此,如果两个这样的声明足够相似,则它们是兼容的。
就是说-是的,严格来说,这是未定义的行为,因为do_stuff函数或其他人将使用具有void*
as参数的函数指针来调用您的函数,但是您的函数具有不兼容的参数。但是,尽管如此,我希望所有编译器都能在不抱怨的情况下进行编译和运行。但是,您可以通过让另一个函数接受一个void*
(并将其注册为回调函数)来做一个更清洁的操作,该函数随后将只调用您的实际函数。