将函数指针转换为其他类型


88

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


1
我有点新手,但是“ void()(void)函数指针”是什么意思?它是一个指向函数接受一个void *作为参数,并返回void
数字加

2
@Myke:void (*func)(void *)表示func具有类型签名(例如)的函数的指针void foo(void *arg)。是的,你是对的。
mk12

Answers:


121

就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调用约定(其中宏CALLBACKPASCALWINAPI所有扩展到)。如果传递使用标准C调用约定(cdecl)的函数指针,则会导致错误。
  • 在C ++中,在类成员函数指针和常规函数指针之间进行强制转换。这通常会使C ++新手绊倒。类成员函数具有一个隐藏this参数,如果将成员函数转换为常规函数,将没有this对象可使用,并且同样会导致很多问题。

另一个坏主意有时可能会起作用,但也是未定义的行为:

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

18
难道不是全部void*要与其他任何指针兼容吗?struct my_struct*将a强制转换为a应该没有问题void*,实际上您甚至不必强制转换,编译器应该接受它。例如,如果将a传递struct my_struct*给采用的函数,则void*无需强制转换。我在这里错过了什么使这些不兼容?
brianmearns 2012年

2
该答案引用了“这可能在x86上可以正常使用...”:是否有任何平台对此不起作用?有人失败时有经验吗?如果可能,用于C的qsort()似乎是投射函数指针的好地方。
kevinarpe 2012年

4
@KCArpe:根据本文“成员函数指针的实现”标题下的图表,在某些配置中,16位OpenWatcom编译器有时使用的函数指针类型(4个字节)比数据指针类型(2个字节)大。但是,符合POSIX的系统必须使用与void*函数指针类型相同的表示形式,请参见spec
亚当·罗森菲尔德

3
@adam中的链接现在引用POSIX标准的2016版,其中删除了第2.12.3节的相关内容。您仍然可以在2008年版中找到它。
马丁·特伦克曼

6
@brianmearns否,void *仅以非常精确定义的方式“与”任何其他(非功能)指针“兼容”(与C标准在这种情况下用“兼容”一词无关)。C允许avoid *大于或小于a struct my_struct *,或者使这些位具有不同的顺序或取反或其他形式。因此void f(void *)void f(struct my_struct *)可以与ABI不兼容。如果需要,C会为您自己转换指针,但是它不会并且有时无法转换指向函数以采用可能不同的参数类型。
mtraceur

32

最近,我就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在具有多种编译器/链接器/内核加载器(GCC / CLang / MSVC)的大量平台(Linux / Solaris / Windows / OS X)上进行编译/工作/测试。我猜这是该死的标准。

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

  1. 如果您正在编写一个回调库,则可以。警告购买者-使用后果自负。
  2. 否则,不要这样做。

编写此响应后,请进行更深入的思考,如果C编译器的代码使用相同的技巧,我也不会感到惊讶。而且,由于(大多数/全部?)现代C编译器是自举的,因此这意味着该技巧是安全的。

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


10
Emscripten LLVM to Javascript编译器绝对不起作用。有关详细信息,请参见github.com/kripken/emscripten/wiki/Asm-pointer-casts
Ben Lings

2
关于Emscripten的更新参考。
ysdx

4
@BenLings发布的链接将在不久的将来断开。它已正式迁至kripken.github.io/emscripten-site/docs/porting/guidelines/...
亚历克斯·赖因金

9

重点实际上不是您是否可以。简单的解决方案是

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

我测试过的所有编译器都将为生成代码my_callback_helper,除非它总是内联的。这绝对不是必需的,因为它倾向于做的唯一事情就是jmp my_callback_function。编译器可能想确保这些函数的地址不同,但是不幸的是,即使该函数用C99标记inline(即“不在乎地址”),它也会这样做。
yyny

我不确定这是正确的。上面另一个回复中的另一条评论(@mtraceur提供)说,a void *的大小甚至可以与a的大小不同struct *(我认为这是错误的,因为否则malloc可能会被破坏,但是该评论有5票赞成票,因此,我对此表示赞赏。如果@mtraceur是正确的,则您编写的解决方案将是不正确的
12:56

@cesss:大小不同根本没有关系。往返的转换void*仍然有效。简而言之,void*可能有更多的位,但是如果将a强制转换struct*void*这些多余的位,则可以为零,而回退可以再次丢弃这些零。
MSalters

@MSalters:我真的不知道void *(理论上)与可能有什么不同struct *。我正在用C实现一个vtable,并且正在使用C-ishthis指针作为虚拟函数的第一个参数。显然,this必须是指向“当前”(派生)结构的指针。因此,虚函数需要根据他们在实施的结构不同的原型我想用一个。void *this论证会解决一切,但现在我学会了它的不确定的行为...
cesss

6

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

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

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


4

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

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

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


2
只要struct-pointer和void-pointer具有兼容的位表示形式,您才是安全的;不能保证是这样
Christoph

1
编译器还可以在寄存器中传递参数。对于浮点数,整数或指针使用不同的寄存器并不是没有听说过的。
MSalters

0

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

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

指向void的指针可以与任何不完整或对象类型的指针相互转换


这与亚当·罗森菲尔德(Adam Rosenfield)的答案相矛盾,请参阅最后一段和评论
用户

1
这个答案显然是错误的。函数指针外,任何指针都可以转换为void指针。
marton78 '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.