将函数指针转换为另一种类型
假设我有一个接受 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标准这说你可以转换为“兼容函数指针”,但我找不到“兼容函数指针”含义的定义。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
如果您考虑一下函数调用在 C/C++ 中的工作方式,它们会将某些项目压入堆栈,跳转到新的代码位置,执行,然后在返回时弹出堆栈。如果您的函数指针描述具有相同返回类型和相同数量/大小的参数的函数,那么应该没问题。
因此,我认为您应该能够安全地这样做。
If you think about the way function calls work in C/C++, they push certain items on the stack, jump to the new code location, execute, then pop the stack on return. If your function pointers describe functions with the same return type and the same number/size of arguments, you should be okay.
Thus, I think you should be able to do so safely.
空指针与其他类型的指针兼容。它是 malloc 和 mem 函数(
memcpy
、memcmp
)工作原理的支柱。通常,在 C(而不是 C++)中NULL
是定义为((void *)0)
的宏。看看C99中的6.3.2.3(第1项):
Void pointers are compatible with other types of pointer. It's the backbone of how malloc and the mem functions (
memcpy
,memcmp
) work. Typically, in C (Rather than C++)NULL
is a macro defined as((void *)0)
.Look at 6.3.2.3 (Item 1) in C99:
就 C 标准而言,如果将函数指针强制转换为不同类型的函数指针,然后调用它,则这是未定义行为。参见附件 J.2(资料性):
第 6.3.2.3 节第 8 段内容如下:
换句话说,您可以将函数指针转换为不同的函数指针类型,再次将其转换回来,然后调用它,事情就会起作用。
兼容的定义有些复杂。可以在第 6.7.5.3 节第 15 段中找到:
确定两种类型是否兼容的规则在第 6.2.7 节中描述,由于它们相当冗长,我不会在这里引用它们,但您可以在 ""> open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf" rel="noreferrer">C99 标准草案 (PDF)。
相关规则见第 6.7.5.1 节第 2 段:
因此,由于
void*
与struct my_struct*
不兼容,void (*)(void*)
类型的函数指针与void (*)(void*)
类型的函数指针不兼容void (*)(结构体my_struct*)
,因此函数指针的这种转换在技术上是未定义的行为。但实际上,在某些情况下,您可以安全地摆脱函数指针的转换。在 x86 调用约定中,参数被压入堆栈,并且所有指针的大小相同(x86 中为 4 字节,x86_64 中为 8 字节)。调用函数指针归结为将参数压入堆栈并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念。
你绝对不能做的事情:
stdcall
调用约定(其中宏CALLBACK
、PASCAL
和WINAPI
所有扩展到)。如果传递使用标准 C 调用约定 (cdecl
) 的函数指针,则会导致错误。this
参数,如果将成员函数转换为常规函数,则没有this
对象可供使用,同样,会导致很多问题。另一个有时可能有效但也是未定义行为的坏主意:
void (*)(void)
转换为void*
) 。函数指针的大小不一定与常规指针相同,因为在某些体系结构上它们可能包含额外的上下文信息。这在 x86 上可能可以正常工作,但请记住这是未定义的行为。As far as the C standard is concerned, if you cast a function pointer to a function pointer of a different type and then call that, it is undefined behavior. See Annex J.2 (informative):
Section 6.3.2.3, paragraph 8 reads:
So in other words, you can cast a function pointer to a different function pointer type, cast it back again, and call it, and things will work.
The definition of compatible is somewhat complicated. It can be found in section 6.7.5.3, paragraph 15:
The rules for determining whether two types are compatible are described in section 6.2.7, and I won't quote them here since they're rather lengthy, but you can read them on the draft of the C99 standard (PDF).
The relevant rule here is in section 6.7.5.1, paragraph 2:
Hence, since a
void*
is not compatible with astruct my_struct*
, a function pointer of typevoid (*)(void*)
is not compatible with a function pointer of typevoid (*)(struct my_struct*)
, so this casting of function pointers is technically undefined behavior.In practice, though, you can safely get away with casting function pointers in some cases. In the x86 calling convention, arguments are pushed on the stack, and all pointers are the same size (4 bytes in x86 or 8 bytes in x86_64). Calling a function pointer boils down to pushing the arguments on the stack and doing an indirect jump to the function pointer target, and there's obviously no notion of types at the machine code level.
Things you definitely can't do:
stdcall
calling convention (which the macrosCALLBACK
,PASCAL
, andWINAPI
all expand to). If you pass a function pointer that uses the standard C calling convention (cdecl
), badness will result.this
parameter, and if you cast a member function to a regular function, there's nothis
object to use, and again, much badness will result.Another bad idea that might sometimes work but is also undefined behavior:
void (*)(void)
to avoid*
). Function pointers aren't necessarily the same size as regular pointers, since on some architectures they might contain extra contextual information. This will probably work ok on x86, but remember that it's undefined behavior.我最近询问了有关 GLib 中某些代码的完全相同的问题。 (GLib 是 GNOME 项目的核心库,用 C 语言编写。)我被告知整个 slot'n'signals 框架都依赖于它。
在整个代码中,有许多从类型 (1) 到类型 (2) 的转换实例:
const void *b)
typedef int (*CompareDataFunc) (const void *b,
常量无效*b,
void *user_data)
使用这样的调用进行链通是很常见的:
在
g_array_sort()
中亲自查看:http://git.gnome.org/browse/glib/tree/glib/garray.c上面的答案很详细可能是正确的—— 如果您是标准委员会的成员。亚当和约翰尼斯的研究深入,值得赞扬。然而,在野外,您会发现这段代码运行得很好。有争议的?是的。考虑一下:GLib 使用各种编译器/链接器/内核加载器 (GCC/CLang/MSVC) 在大量平台 (Linux/Solaris/Windows/OS X) 上进行编译/工作/测试。我想,标准该死。
我花了一些时间思考这些答案。这是我的结论:
写完这个回复后,深入思考一下,如果 C 编译器的代码使用同样的技巧,我不会感到惊讶。由于(大多数/全部?)现代 C 编译器是自举的,这意味着该技巧是安全的。
一个更重要的研究问题:有人能找到一个平台/编译器/链接器/加载器,这个技巧在其中不起作用吗?对此的主要印象分。我敢打赌有一些嵌入式处理器/系统不喜欢它。然而,对于桌面计算(可能还有移动/平板电脑),这个技巧可能仍然有效。
I asked about this exact same issue regarding some code in GLib recently. (GLib is a core library for the GNOME project and written in C.) I was told the entire slots'n'signals framework depends upon it.
Throughout the code, there are numerous instances of casting from type (1) to (2):
typedef int (*CompareFunc) (const void *a,
const void *b)
typedef int (*CompareDataFunc) (const void *b,
const void *b,
void *user_data)
It is common to chain-thru with calls like this:
See for yourself here in
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cThe answers above are detailed and likely correct -- if you sit on the standards committee. Adam and Johannes deserve credit for their well-researched responses. However, out in the wild, you will find this code works just fine. Controversial? Yes. Consider this: GLib compiles/works/tests on a large number of platforms (Linux/Solaris/Windows/OS X) with a wide variety of compilers/linkers/kernel loaders (GCC/CLang/MSVC). Standards be damned, I guess.
I spent some time thinking about these answers. Here is my conclusion:
Thinking deeper after writing this response, I would not be surprised if the code for C compilers uses this same trick. And since (most/all?) modern C compilers are bootstrapped, this would imply the trick is safe.
A more important question to research: Can someone find a platform/compiler/linker/loader where this trick does not work? Major brownie points for that one. I bet there are some embedded processors/systems that don't like it. However, for desktop computing (and probably mobile/tablet), this trick probably still works.
关键不在于你是否可以。简单的解决方案是,
一个好的编译器只会在确实需要时为 my_callback_helper 生成代码,在这种情况下,您会很高兴它这样做了。
The point really isn't whether you can. The trivial solution is
A good compiler will only generate code for my_callback_helper if it's really needed, in which case you'd be glad it did.
如果返回类型和参数类型兼容,那么您就有一个兼容的函数类型 - 基本上(实际上更复杂:))。兼容性与“相同类型”相同,只是更宽松,允许有不同的类型,但仍然有某种形式的说法“这些类型几乎相同”。例如,在 C89 中,如果两个结构在其他方面相同但只是名称不同,则它们是兼容的。 C99 似乎改变了这一点。引用c 基本原理文档(强烈推荐阅读,顺便说一句!):
也就是说 - 是的,严格来说这是未定义的行为,因为您的 do_stuff 函数或其他人将使用以
void*
作为参数的函数指针调用您的函数,但您的函数有一个不兼容的参数。但尽管如此,我希望所有编译器都能毫无抱怨地编译和运行它。但是您可以通过让另一个函数采用void*
(并将其注册为回调函数)来做到更干净,然后该函数将只调用您的实际函数。You have a compatible function type if the return type and parameter types are compatible - basically (it's more complicated in reality :)). Compatibility is the same as "same type" just more lax to allow to have different types but still have some form of saying "these types are almost the same". In C89, for example, two structs were compatible if they were otherwise identical but just their name was different. C99 seem to have changed that. Quoting from the c rationale document (highly recommended reading, btw!):
That said - yeah strictly this is undefined behavior, because your do_stuff function or someone else will call your function with a function pointer having
void*
as parameter, but your function has an incompatible parameter. But nevertheless, i expect all compilers to compile and run it without moaning. But you can do cleaner by having another function taking avoid*
(and registering that as callback function) which will just call your actual function then.由于 C 代码编译为根本不关心指针类型的指令,因此使用您提到的代码是很好的。当您使用回调函数运行 do_stuff 并指向其他内容然后将 my_struct 结构作为参数时,您会遇到问题。
我希望我可以通过显示什么不起作用来使其更清楚:
或者...
基本上,只要数据在运行时继续有意义,您就可以将指针强制转换为您喜欢的任何内容。
As C code compiles to instruction which do not care at all about pointer types, it's quite fine to use the code you mention. You'd run into problems when you'd run do_stuff with your callback function and pointer to something else then my_struct structure as argument.
I hope I can make it clearer by showing what would not work:
or...
Basically, you can cast pointers to whatever you like, as long as the data continue to make sense at run-time.
好吧,除非我理解错误,否则你可以用这种方式转换函数指针。
更简洁的方法是创建一个函数 typedef。
Well, unless I understood the question wrong, you can just cast a function pointer this way.
A cleaner way would be to create a function typedef.
正如其他一些答案/评论所解释的那样,这是未定义的行为。
那么它将是明确定义的
但是,如果您将 void my_callback_function(struct my_struct* arg) 替换为假设您在通过函数指针调用函数时确实将指向 my_struct 对象的指针作为参数传递,
。确实,指向任何对象的指针都可以转换为 void,然后转换回原始类型,而不会改变其值,但这只能保证像我上面建议的函数有效,而不是 void my_callback_function( struct my_struct* arg) 人们倾向于相信它们实际上是相同的。
It's undefined behaviour as some other answers/comments have explained.
But it would be well-defined if you replace
void my_callback_function(struct my_struct* arg)
withassuming that you really passed a pointer to a my_struct object as an argument when calling the function by the function pointer.
It is true that a pointer to any object can be cast to void and then cast back to the original type without any change in its value, but this can only guarantee that a function like I suggested above works, not
void my_callback_function(struct my_struct* arg)
People tend to believe that they are actually the same.