【问题标题】:Casting a function pointer to another type将函数指针转换为另一种类型
【发布时间】:2010-10-08 06:11:11
【问题描述】:

假设我有一个接受 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);

我查看了 this question 并查看了一些 C 标准,这些标准说您可以强制转换为“兼容函数指针”,但我找不到“兼容函数指针”含义的定义。

【问题讨论】:

  • 我是个新手,但“void ()(void) 函数指针”是什么意思?它是一个指向接受 void* 作为参数并返回 void 的函数的指针吗
  • @Myke: void (*func)(void *) 表示func 是一个指向具有诸如void foo(void *arg) 之类的类型签名的函数的指针。所以是的,你是对的。

标签: c function-pointers


【解决方案1】:

就 C 标准而言,如果将函数指针转换为不同类型的函数指针,然后调用它,则 未定义行为。见附件 J.2(资料性):

在以下情况下行为未定义:

  • 指针用于调用类型与指向的函数不兼容的函数 类型(6.3.2.3)。

第 6.3.2.3 节第 8 段内容如下:

指向一种函数的指针可以转换为指向另一种函数的指针 键入并再次返回;结果应与原始指针比较。如果一个转换 指针用于调用类型与指向类型不兼容的函数, 行为未定义。

所以换句话说,你可以将一个函数指针转换为不同的函数指针类型,再次将它转换回来,然后调用它,一切都会奏效。

兼容的定义有些复杂。可在第 6.7.5.3 节第 15 段中找到:

对于要兼容的两种函数类型,两者都应指定兼容的返回类型127

此外,参数类型列表,如果两者都存在,则应在数量上一致 参数和省略号终止符的使用;相应的参数应有 兼容的类型。如果一种类型具有参数类型列表,而另一种类型由 不属于函数定义且包含空的函数声明符 标识符列表,参数列表不应有省略号终止符和每个的类型 参数应与应用程序产生的类型兼容 默认参数促销。如果一种类型有参数类型列表,而另一种类型是 由包含(可能为空的)标识符列表的函数定义指定,两者都应 参数个数一致,每个原型参数的类型为 与应用默认参数产生的类型兼容 促销到相应标识符的类型。 (在确定类型 兼容性和复合类型,每个参数用函数或数组声明 type 被视为具有调整后的类型,并且每个参数都声明为限定类型 被视为具有其声明类型的非限定版本。)

127) 如果两个函数类型都是“旧式”,则不比较参数类型。

判断两种类型是否兼容的规则在 6.2.7 节中有描述,这里不再引用它们,因为它们比较长,但是你可以在draft of the C99 standard (PDF) 上阅读它们。

这里的相关规则在第 6.7.5.1 节第 2 段:

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

因此,由于 void* is not compatiblestruct 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 上可以正常工作,但请记住这是未定义的行为。

【讨论】:

  • void* 的全部意义不在于它们与任何其他指针兼容吗?将struct my_struct* 转换为void* 应该没有问题,实际上您甚至不必转换,编译器应该只接受它。例如,如果您将 struct my_struct* 传递给采用 void* 的函数,则不需要强制转换。我在这里缺少什么使这些不兼容?
  • 这个答案提到“这可能在 x86 上可以正常工作......”:是否有任何平台无法正常工作?有人在失败时有经验吗?如果可能的话,C 的 qsort() 似乎是一个转换函数指针的好地方。
  • @KCArpe:根据this article 中“成员函数指针的实现”标题下的图表,16 位 OpenWatcom 编译器有时会使用比数据指针更大的函数指针类型(4 字节)在某些配置中键入(2 个字节)。但是,符合 POSIX 的系统必须对 void* 使用与函数指针类型相同的表示,请参阅 the spec
  • @adam 的链接现在指的是 POSIX 标准的 2016 版,其中删除了相关的 2.12.3 部分。您仍然可以在2008 edition 中找到它。
  • @brianmearns 不,void * 仅与任何其他(非函数)指针以非常精确定义的方式“兼容”(与 C 标准无关在这种情况下表示“兼容”一词)。 C 允许void *struct my_struct * 更大或更小,或者使位具有不同的顺序或取反或其他。所以void f(void *)void f(struct my_struct *) 可以是ABI 不兼容。如果需要,C 会为您自己转换指针,但它不会而且有时无法转换指向函数以采用可能不同的参数类型。
【解决方案2】:

我最近询问了关于 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 编译器是自举的,这意味着这个技巧是安全的。

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

【讨论】:

【解决方案3】:

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

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

【讨论】:

  • 问题是这不是一个通用的解决方案。它需要在了解功能的情况下逐案完成。如果你已经有一个错误类型的函数,你就卡住了。
  • 我测试过的所有编译器都会为my_callback_helper生成代码,除非它总是内联的。这绝对没有必要,因为它唯一倾向于做的是jmp my_callback_function。编译器可能希望确保函数的地址不同,但不幸的是,即使函数标记为 C99 inline(即“不关心地址”),它也会这样做。
  • 我不确定这是否正确。上面另一个回复(@mtraceur)的另一条评论说,void * 的大小甚至可以与 struct * 不同(我认为这是错误的,因为否则 malloc 会被破坏,但该评论有 5 个赞成票,所以我给了它一些信任。如果@mtraceur 是正确的,那么您编写的解决方案将不正确。
  • @cesss:大小不一样也没关系。与void* 之间的转换仍然需要进行。简而言之,void* 可能有更多位,但如果您将struct* 转换为void*,那么这些额外的位可能是零,而转换回的位可以再次丢弃这些零。
  • @MSalters:我真的不知道void * 可以(理论上)与struct * 如此不同。我在 C 中实现了一个 vtable,并且我使用了一个 C++-ish this 指针作为虚函数的第一个参数。显然,this 必须是指向“当前”(派生)结构的指针。因此,虚函数需要不同的原型,具体取决于它们在其中实现的结构。我认为使用 void *this 参数可以解决所有问题,但现在我了解到这是未定义的行为......
【解决方案4】:

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

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

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

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

      • 这与Adam Rosenfield's answer相矛盾,见最后一段和cmets
      • 这个答案显然是错误的。任何指针都可以转换为 void 指针,除了函数指针。
      【解决方案7】:

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

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

      【讨论】:

      • 只有struct-pointers 和void-pointers 具有兼容的位表示,你才是安全的;不保证是这种情况
      • 编译器也可以在寄存器中传递参数。并且为浮点数、整数或指针使用不同的寄存器并非闻所未闻。
      猜你喜欢
      • 2020-12-14
      • 1970-01-01
      • 2012-03-14
      • 2016-10-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多