将函数指针转换为另一种类型

116

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


2
我有些新手,但什么是“void ()(void)函数指针”?它是一个指向接受void*作为参数并返回void的函数的指针吗? - Mykie
2
@Myke: void (*func)(void *) 的意思是,func 是一个指向函数的指针,其类型签名类似于 void foo(void *arg)。所以,你说得对。 - mk12
9个回答

140
就C标准而言,如果你把一个函数指针转换成不同类型的函数指针,然后调用它,那么这是未定义的行为。请参见Annex J.2(说明性):
“在以下情况下行为未定义:使用指向与所指类型(6.3.2.3)不兼容的函数调用指针。”
第6.3.2.3节第8段的内容如下:
“一个函数指针可以被转换为另一种函数指针类型,再转换回来;结果应该等同于原始指针。如果使用转换后的指针调用其所指类型不兼容的函数,则行为未定义。”
因此,换句话说,你可以将函数指针强制转换为不同的函数指针类型,然后再强制转换回来并调用它,事情就会正常进行。
“兼容”的定义有些复杂。可以在第6.7.5.3节第15段中找到相关内容:
为了使两个函数类型兼容,它们都必须指定兼容的返回类型。此外,如果两个函数类型都有参数类型列表,则它们应在参数数量和省略号使用上达成一致;相应的参数应具有兼容的类型。如果一个函数类型有参数类型列表,而另一个函数类型由不是函数定义的函数声明符指定,并且包含空标识符列表,则参数列表不得有省略号,每个参数的类型都应与默认参数提升的结果类型兼容。如果一个函数类型有参数类型列表,而另一个函数类型由包含(可能为空的)标识符列表的函数定义指定,则它们应在参数数量上达成一致,每个原型参数的类型应与对应标识符的类型应用默认参数提升后的结果类型兼容。(在确定类型兼容性和组合类型时,每个以函数或数组类型声明的参数均视为具有调整后的类型,每个以限定类型声明的参数均视为其声明类型的未限定版本。)如果两个函数类型都是“旧式”的,则不比较参数类型。确定两个类型是否兼容的规则在第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 调用约定(宏 CALLBACKPASCALWINAPI 都会扩展为此)。如果您传递了一个使用标准 C 调用约定 (cdecl) 的函数指针,那么将会出现问题。
  • 在 C++ 中,类成员函数指针和常规函数指针之间进行转换。这经常使 C++ 新手犯错。类成员函数有一个隐藏的 this 参数,如果您将成员函数转换为常规函数,则没有可用的 this 对象,结果也会很糟糕。

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

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

24
void* 的整个意义不就是它们与任何其他指针兼容吗?将 struct my_struct* 强制转换为 void* 不应该有问题,实际上你甚至不需要进行强制转换,编译器应该会直接接受它。例如,如果你将一个 struct my_struct* 传递给一个需要 void* 的函数,无需进行强制转换。那么这里有什么遗漏导致它们不兼容呢? - brianmearns
3
这个答案提到了“This will probably work ok on x86...”,那么有没有任何平台无法运行呢?有人有失败的经验吗?如果可能的话,C语言中的qsort()函数似乎是一个很好的地方来进行函数指针转换。 - kevinarpe
4
根据 这篇文章 中标题为“成员函数指针的实现”的图表显示,16位的OpenWatcom编译器在某些情况下会使用比数据指针类型(2个字节)更大的函数指针类型(4个字节)。然而,符合POSIX标准的系统必须使用与函数指针类型相同的表示方式来表示 void*,详见规范 - Adam Rosenfield
3
@adam的链接现在指向了POSIX标准的2016版,在相关的2.12.3章节中已被删除。你仍然可以在2008版中找到它。 - Martin Trenkmann
9
不,void * 只有在非常明确定义的情况下与其他(非函数)指针兼容(这与 C 标准在此情况下使用“兼容”一词的含义无关)。C 允许 void *struct my_struct * 更大或更小,或者具有不同顺序的位或被取反等。因此,void f(void *)void f(struct my_struct *) 可能是ABI不兼容的。如果需要,C将为您转换指针本身,但它不会并且有时不能将所指向的函数转换为可能具有不同参数类型的函数。 - mtraceur
显示剩余7条评论

41

最近我问了关于GLib代码中的一个问题,这是一个用C语言编写的GNOME项目的核心库。有人告诉我整个slots'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. 如果您正在编写回调库,那么这可能没问题。Caveat emptor-自负风险。
  2. 否则,请不要这样做。

在撰写此响应后深入思考,我不会惊讶,如果C编译器的代码也使用相同的技巧。由于(大多数/全部?)现代C编译器都是自引导的,这意味着这个技巧是安全的。

一个更重要的研究问题:有人能找到这个技巧不起作用的平台/编译器/链接器/加载程序吗?对于这一点将获得重要的加分。我相信有一些嵌入式处理器/系统不喜欢它。但是,对于桌面计算机(以及可能的移动/平板电脑),这个技巧可能仍然有效。


12
绝对行不通的地方是Emscripten LLVM转Javascript编译器。请参见https://github.com/kripken/emscripten/wiki/Asm-pointer-casts获取详细信息。 - Ben Lings
2
关于Emscripten的更新参考。 - ysdx
4
@BenLings发布的链接将在不久的将来失效。官方已将其移至http://kripken.github.io/emscripten-site/docs/porting/guidelines/function_pointer_issues.html。 - Alex Reinking

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生成代码,这样你会很高兴它这样做。


问题在于这不是一种通用的解决方案。需要针对每个情况,具有函数知识来完成。如果你已经有了错误类型的函数,那么就会卡住。 - BeeOnRope
1
我测试过的所有编译器都会为my_callback_helper生成代码,除非它总是内联的。这绝对不是必要的,因为它倾向于做的唯一事情就是jmp my_callback_function。编译器可能想确保函数的地址不同,但不幸的是,即使将函数标记为C99 inline(即“不关心地址”),它仍会这样做。 - yyny
1
我不确定这是否正确。另一个回复(由@mtraceur)中的另一个评论说,void *甚至可以比struct *大小不同(我认为这是错误的,因为否则malloc将无法正常工作,但该评论已经获得了5个赞,所以我会给它一些信任度)。如果@mtraceur是正确的,那么您编写的解决方案将不正确。 - cesss
@cesss:大小不同一点也不重要。转换到和从 void* 仍然必须工作。简而言之,void* 可能有更多的位,但如果你将 struct* 强制转换为 void*,那些额外的位可以是零,再次强制转换时可以丢弃这些零。 - MSalters
1
@MSalters:我真的不知道void *在理论上可以与struct *如此不同。我正在用C实现一个vtable,并且将C++风格的this指针作为虚函数的第一个参数。显然,this必须是指向“当前”(派生)结构体的指针。因此,虚函数需要根据它们所实现的结构体具有不同的原型。我以为使用void *this参数会解决所有问题,但现在我学到了这是未定义的行为... - cesss

8
如果返回类型和参数类型是兼容的,那么你就有了一个兼容的函数类型 - 基本上(实际上更加复杂:))。兼容性与“相同类型”相同,只是更加宽松,以允许具有不同类型但仍具有某种形式的“这些类型几乎相同”的情况。例如,在C89中,如果两个结构体在其他方面完全相同,只是名称不同,则它们是兼容的。似乎C99已经改变了这一点。引用自C理由文件(强烈推荐阅读!):

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

也就是说——是的,严格而言,这是未定义的行为,因为你的do_stuff函数或其他人将使用一个函数指针调用你的函数,其中该函数指针具有void*作为参数,但是你的函数具有不兼容的参数。但尽管如此,我希望所有编译器都能编译并运行它而不会抱怨。但你可以通过编写另一个接受void*的函数(并将其注册为回调函数),该函数将仅调用你的实际函数进行处理,以使代码更加清晰。


3

由于C代码编译为指令时完全不关心指针类型,因此使用您提到的代码是可以的。当您使用回调函数和指向my_struct结构以外的指针作为参数运行do_stuff时,会遇到问题。

我希望通过展示哪些内容不起作用来使它更清晰:

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

基本上,只要在运行时数据仍然有意义,您可以将指针转换为任何您喜欢的内容。


0

嗯,除非我理解错了问题,否则你可以通过这种方式转换函数指针。

void print_data(void *data)
{
    // ...
}


((void (*)(char *)) &print_data)("hello");

一个更加清晰的方式是创建一个函数typedef。
typedef void(*t_print_str)(char *);
((t_print_str) &print_data)("hello");

0
这是未定义的行为,正如其他答案/评论所解释的那样。
但是,如果你将void my_callback_function(struct my_struct* arg)替换为其他内容,它将变得明确定义。
void my_callback_function(void *arg)
{
   struct my_struct* p = arg;
   ... // use p
}

假设在调用函数指针时,您确实将指针传递给了一个my_struct对象作为参数。
指针可以被转换为void类型,然后再转换回原始类型而不改变其值,这是正确的。但是这只能保证像我上面建议的那样的函数能够正常工作,而不能保证void my_callback_function(struct my_struct* arg)这样的函数也能正常工作。人们往往错误地认为它们是相同的。

-1

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

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

指向void的指针可以转换为任何不完整或对象类型的指针,反之亦然。


这与Adam Rosenfield的回答相矛盾,请参见最后一段和评论。 - user
2
这个答案显然是错误的。任何指针都可以转换为void指针,除了函数指针。 - marton78

-1

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

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


2
只有当“struct”指针和“void”指针具有兼容的位表示时,您才是安全的;这并不保证一定会发生。 - Christoph
1
编译器也可以在寄存器中传递参数。对于浮点数、整数或指针使用不同的寄存器也并非罕见。 - MSalters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接