105

假设我有一个接受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 标准,这些标准说您可以强制转换为“兼容函数指针”,但我找不到“兼容函数指针”含义的定义。

4

7 回答 7

136

就 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 段中:

对于要兼容的两种指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。

因此,由于 a与 avoid* struct my_struct*兼容,因此类型的函数指针与类型void (*)(void*)的函数指针不兼容void (*)(struct my_struct*),因此函数指针的这种转换在技术上是未定义的行为。

但是,在实践中,在某些情况下,您可以安全地摆脱强制转换函数指针。在 x86 调用约定中,参数被压入堆栈,所有指针大小相同(x86 中为 4 字节,x86_64 中为 8 字节)。调用函数指针归结为将参数压入堆栈并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念。

绝对不能做的事:

  • 在不同调用约定的函数指针之间转换。你会弄乱堆栈,充其量会崩溃,最坏的情况是,通过一个巨大的安全漏洞默默地成功。在 Windows 编程中,您经常传递函数指针。Win32 期望所有回调函数都使用stdcall调用约定(宏CALLBACKPASCALWINAPI所有都扩展为)。如果传递使用标准 C 调用约定 ( cdecl) 的函数指针,将导致错误。
  • 在 C++ 中,在类成员函数指针和常规函数指针之间进行强制转换。这经常会绊倒 C++ 新手。类成员函数有一个隐藏this参数,如果将成员函数转换为常规函数,就没有this对象可以使用,同样会导致很多错误。

另一个有时可能有效但也是未定义行为的坏主意:

  • 在函数指针和常规指针之间进行强制转换(例如将 a 强制转换void (*)(void)为 a void*)。函数指针不一定与常规指针大小相同,因为在某些架构上它们可能包含额外的上下文信息。这可能在 x86 上可以正常工作,但请记住这是未定义的行为。
于 2009-02-18T02:54:36.093 回答
35

我最近询问了有关 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 ://git.gnome.org/browse/glib/tree/glib/garray.c

上面的答案很详细,而且很可能是正确的——如果你是标准委员会的成员的话。Adam 和 Johannes 值得称赞他们经过充分研究的回应。但是,在野外,您会发现此代码运行良好。有争议的?是的。考虑一下:GLib 在大量平台(Linux/Solaris/Windows/OS X)上使用各种编译器/链接器/内核加载器(GCC/CLang/MSVC)编译/工作/测试。标准该死,我猜。

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

  1. 如果您正在编写回调库,这可能没问题。警告购买者 - 使用风险自负。
  2. 否则,不要这样做。

在写下这个回复之后深入思考,如果 C 编译器的代码使用相同的技巧,我不会感到惊讶。而且由于(大多数/全部?)现代 C 编译器是自举的,这意味着这个技巧是安全的。

一个更重要的研究问题:有人能找到这个技巧不起作用的平台/编译器/链接器/加载器?那个主要的布朗尼点。我敢打赌,有些嵌入式处理器/系统不喜欢它。然而,对于桌面计算(可能还有移动/平板电脑),这个技巧可能仍然有效。

于 2012-12-26T17:56:42.157 回答
12

关键不在于你能不能。简单的解决方案是

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 生成代码,在这种情况下你会很高兴它这样做了。

于 2009-02-18T13:27:30.427 回答
6

如果返回类型和参数类型兼容,则您有一个兼容的函数类型 - 基本上(实际上它更复杂:))。兼容性与“相同类型”相同,只是更宽松以允许拥有不同类型,但仍然有某种形式说“这些类型几乎相同”。例如,在 C89 中,如果两个结构在其他方面相同但只是名称不同,则它们是兼容的。C99 似乎改变了这一点。引用c 基本原理文档(强烈推荐阅读,顺便说一句!):

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

也就是说 - 是的,严格来说这是未定义的行为,因为您的 do_stuff 函数或其他人将使用具有void*作为参数的函数指针调用您的函数,但您的函数具有不兼容的参数。但是,尽管如此,我希望所有编译器都能编译和运行它而不会抱怨。但是你可以通过让另一个函数接受一个void*(并将其注册为回调函数)来做更清洁,然后它只会调用你的实际函数。

于 2009-02-18T03:02:31.210 回答
3

由于 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

基本上,只要数据在运行时继续有意义,您就可以将指针投射到您喜欢的任何地方。

于 2009-02-18T02:19:10.127 回答
0

void 指针与其他类型的指针兼容。它是 malloc 和 mem 函数 ( memcpy, memcmp) 工作方式的支柱。通常,在 C(而不是 C++)NULL中,宏定义为((void *)0).

查看 C99 中的 6.3.2.3(第 1 项):

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

于 2011-01-19T14:36:16.093 回答
-1

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

因此,我认为您应该能够安全地这样做。

于 2009-02-18T02:21:16.970 回答