将函数指针投射到另一个类型

比方说,我有一个函数接受一个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*)类型的函数指针不兼容,所以此Cast函数指针在技术上是未定义的行为。

实际上,在某些情况下,你可以放心地使用转换函数指针。 在x86调用约定中,参数被压入堆栈,并且所有指针的大小相同(x86中4个字节或x86_64中8个字节)。 调用一个函数指针归结为在堆栈上推送参数并间接跳转到函数指针目标,在机器代码级别显然没有类型的概念。

你绝对不能做的事情:

  • 在不同调用约定的函数指针之间进行转换。 你会搞砸堆栈,最好的是,崩溃,最坏的情况下,一个巨大的安全漏洞默默成功。 在Windows编程中,你经常会传递函数指针。 Win32希望所有的回调函数都使用stdcall调用约定(宏CALLBACKPASCALWINAPI全部扩展到)。 如果您传递使用标准C调用约定( cdecl )的函数指针,则会导致不良情况。
  • 在C ++中,在类成员函数指针和常规函数指针之间进行转换。 这经常绊倒C ++新手。 类成员函数有一个隐藏this参数,如果你将一个成员函数转换成一个常规函数,那么就不会使用this对象,并且会导致很多不好的结果。

另一个不好的想法,有时可能工作,但也是未定义的行为:

  • 在函数指针和常规指针之间进行转换(例如,将void (*)(void)投射到void* )。 函数指针不一定与常规指针大小相同,因为在某些体系结构中,它们可能包含额外的上下文信息。 这可能会在x86上正常工作,但请记住它是未定义的行为。

我最近问了关于GLib中一些代码的完全相同的问题。 (GLib是GNOME项目的核心库,用C语言编写)。我被告知整个slot'n'signals框架依赖于它。

在整个代码中,从(1)到(2)类型的铸造有很多例子:

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. 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 : g_array_sort()

上面的答案是详细的,可能是正确的 – 如果你坐在标准委员会。 Adam和Johannes因其经过深入研究的回应而值得称赞。 然而,在外面,你会发现这个代码工作得很好。 争议? 是。 考虑一下:GLib用各种各样的编译器/链接器/内核加载器(GCC / CLang / MSVC)在大量的平台(Linux / Solaris / Windows / OS X)上编译/工作/测试。 标准是该死的,我猜。

我花了一些时间思考这些答案。 这是我的结论:

  1. 如果你正在编写一个回调库,这可能是好的。 注意事项 – 使用风险自负。
  2. 否则,不要这样做。

在编写这个响应后深入思考,如果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生成代码,如果真的需要的话,那么你会很高兴。

由于C代码编译为不关心指针类型的指令,所以使用你提到的代码是相当好的。 当你使用你的回调函数运行do_stuff,并且指向其他的东西,然后使用my_struct结构作为参数时,你会遇到问题。

我希望我可以通过显示什么是不行的更清楚:

 int my_number = 14; do_stuff((void (*)(void*)) &my_callback_function, &my_number); // my_callback_function will try to access int as struct my_struct // and go nuts 

要么…

 void another_callback_function(struct my_struct* arg, int arg2) { something } do_stuff((void (*)(void*)) &another_callback_function, NULL); // another_callback_function will look for non-existing second argument // on the stack and go nuts 

基本上,只要数据在运行时有意义,就可以将指针指向任何你喜欢的东西。

你有一个兼容的函数类型,如果返回类型和参数类型是兼容的 – 基本上(这是更复杂的现实:))。 兼容性与“相同类型”相同只是允许有不同的类型,但仍然有某种形式的“这些类型几乎是相同的”。 例如,在C89中,如果两个结构完全相同,但名称不同,则两个结构是相容的。 C99似乎改变了这一点。 引用c的原理文档 (强烈推荐阅读,btw!):

在两个不同的翻译单元中,结构,联合或枚举类型声明不会正式声明相同的类型,即使这些声明的文本来自相同的包含文件,因为翻译单元本身是不相交的。 因此,标准规定了这些类型的附加兼容性规则,以便如果两个这样的声明足够相似,则它们是兼容的。

这是说 – 是的严格这是未定义的行为,因为你的do_stuff函数或其他人会用一个函数指针具有void*作为参数调用你的函数,但你的函数有一个不兼容的参数。 但是,尽管如此,我希望所有的编译器能够编译并运行它,而不是呻吟。 但是你可以通过让另一个函数带一个void* (并注册为回调函数)来实现清理,然后调用你的实际函数。

如果考虑函数调用在C / C ++中的工作方式,他们会推送堆栈中的某些项目,跳转到新的代码位置,执行,然后在返回时弹出堆栈。 如果你的函数指针描述的函数具有相同的返回类型和相同的参数数目/大小,你应该没问题。

因此,我认为你应该能够安全地做到这一点。

无效指针与其他类型的指针兼容。 这是malloc和mem函数( memcpymemcmp )的工作原理。 通常,在C(而不是C ++)中, NULL是一个定义为((void *)0)的宏。

请看C99中的6.3.2.3(项目1):

指向void的指针可以被转换为指针或指向任何不完整或对象类型的指针

结构* – >无效*反之亦然在我看来可以安全地转换。

在我看来,最大的问题是在__stdcall中有不同数量和大小的参数(如果不是在回调中使用的类型最多的话,那么最大的问题是)。 函数获取由调用者推送到堆栈上的变量,然后函数将自己弹出堆栈。 如果大小不同的应用程序会崩溃 只有返回值不是那么重要,因为通常通过EAX / RAX返回,所以你甚至可以将它转换为BYTE。

最好的是__fastcall(和x68-64上的__stdcall),因为值是通过寄存器而不是堆栈传递的,或者是__cdecl,因为调用者(不是函数)清除了堆栈。 在我看来,它可能是最安全的转换。 可以在atexit()回调机制中看到。 如果指向真实函数(void)的指针将通过将指针作为函数(具有大量参数)来调用,则这是100%安全的。 调用者推动堆栈,调用者弹出堆栈。 参数被忽略。 但它不会以相反的方式工作。

可能是因为这个int __cdecl main(); 可以用或不用argc和argv声明?