18

我使用函数指针的结构来实现不同后端的接口。签名非常不同,但返回值几乎都是 void、void * 或 int。


struct my_interface {
    void  (*func_a)(int i);
    void *(*func_b)(const char *bla);
    ...
    int   (*func_z)(char foo);
};

但并不要求后端支持每个接口函数的函数。所以我有两种可能性,第一种选择是在每次调用之前检查指针是否不等于 NULL。我不太喜欢这样,因为它的可读性和我担心性能影响(但是我没有测量它)。另一种选择是有一个虚拟函数,在极少数情况下接口函数不存在。

因此,每个签名都需要一个虚拟函数,我想知道是否有可能只有一个用于不同的返回值。并将其转换为给定的签名。


#include <stdio.h>

int nothing(void) {return 0;}

typedef int (*cb_t)(int);

int main(void)
{
    cb_t func;
    int i;

    func = (cb_t) nothing;
    i = func(1);

    printf("%d\n", i);

    return 0;
}

我用 gcc 测试了这段代码,它可以工作。但它是理智的吗?或者它会破坏堆栈还是会导致其他问题?

编辑:感谢所有答案,经过进一步阅读,我现在学到了很多关于调用约定的知识。现在对幕后发生的事情有了更好的了解。

4

7 回答 7

16

根据 C 规范,转换函数指针会导致未定义的行为。事实上,有一段时间,GCC 4.3 的预发布版本会在你转换函数指针时返回 NULL,这在规范中完全有效,但他们在发布前取消了该更改,因为它破坏了很多程序。

假设 GCC 继续做它现在所做的事情,它将在默认的 x86 调用约定(以及大多数架构上的大多数调用约定)下正常工作,但我不会依赖它。在每个调用点针对 NULL 测试函数指针并不比函数调用贵多少。如果你真的想要,你可以写一个宏:

#define CALL_MAYBE(func, args...) do {if (func) (func)(## args);} while (0)

或者您可以为每个签名使用不同的虚拟函数,但我可以理解您希望避免这种情况。

编辑

查尔斯·贝利(Charles Bailey)为此打电话给我,所以我去查找了细节(而不是依靠我的空洞记忆)。C规范

766 指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;
767 结果应与原始指针比较。
768 如果转换后的指针用于调用类型与指向的类型不兼容的函数,则行为未定义。

和 GCC 4.2 预发行版(这是在 4.3 之前解决的)遵循这些规则:函数指针的强制转换不会导致 NULL,正如我所写的,而是试图通过不兼容的类型调用函数,即

func = (cb_t)nothing;
func(1);

从您的示例中,将导致abort. 他们改回了 4.1 的行为(允许但警告),部分原因是此更改破坏了 OpenSSL,但同时 OpenSSL 已得到修复,这是编译器可以随时自由更改的未定义行为。

OpenSSL 只是将函数指针转换为其他函数类型,获取并返回相同数量的相同大小的值,而这(假设您不处理浮点数)恰好在所有平台和调用约定中都是安全的了解。但是,其他任何事情都可能不安全。

于 2008-10-09T20:42:45.960 回答
3

我怀疑你会得到一个未定义的行为。

您可以(使用正确的强制转换)将一个指向函数的指针分配给另一个指向具有不同签名的函数的指针,但是当您调用它时,可能会发生奇怪的事情。

您的nothing()函数不带参数,对于编译器来说,这可能意味着他可以优化堆栈的使用,因为那里没有参数。但是在这里你用一个参数来调用它,这是一个意想不到的情况,它可能会崩溃。

我在标准中找不到合适的点,但我记得它说你可以转换函数指针,但是当你调用结果函数时,你必须使用正确的原型,否则行为是未定义的。

作为旁注,您不应该将函数指针与数据指针(如 NULL)进行比较,因为它们可能属于不同的地址空间。C99 标准中有一个附录允许这种特定情况,但我认为它没有得到广泛实施。也就是说,在只有一个地址空间的架构上,将函数指针转换为数据指针或将其与 NULL 进行比较,通常会起作用。

于 2008-10-09T19:48:34.417 回答
1

您确实冒着导致堆栈损坏的风险。话虽如此,如果您声明带有extern "C"链接的函数(和/或__cdecl取决于您的编译器),您可能能够摆脱这种情况。这将类似于函数的方式,例如printf()可以根据调用者的判断采用可变数量的参数。

这在您当前的情况下是否有效也可能取决于您使用的确切编译器选项。如果您使用的是 MSVC,那么调试与发布编译选项可能会产生很大的不同。

于 2008-10-09T19:39:06.680 回答
0

应该没问题。由于调用者负责在调用后清理堆栈,因此不应在堆栈上留下任何额外内容。被调用者(在这种情况下为nothing())是可以的,因为它不会尝试使用堆栈上的任何参数。

编辑:这确实假设 cdecl 调用约定,这通常是 C 的默认值。

于 2008-10-09T19:37:13.143 回答
0

只要您可以保证您正在使用使调用者平衡堆栈而不是被调用者 (__cdecl) 的方法进行调用。如果您没有指定调用约定,则可以将全局约定设置为其他内容。(__stdcall 或 __fastcall)这两者都可能导致堆栈损坏。

于 2008-10-09T19:49:51.073 回答
0

除非您使用特定于实现/特定于平台的东西来强制正确的调用约定,否则这将不起作用。对于某些调用约定,被调用函数负责清理堆栈,因此它们必须知道推送了什么。

我会去检查 NULL 然后打电话 - 我无法想象它会对性能产生任何影响。

计算机检查 NULL 的速度与它们所做的任何事情一样快。

于 2008-10-09T21:00:06.223 回答
-1

C 标准明确不支持将函数指针强制转换为 NULL。你受编译器作者的摆布。它在很多编译器上都可以正常工作。

C 语言最大的烦恼之一是函数指针没有 NULL 或 void* 等价物。

如果你真的希望你的代码是防弹的,你可以声明你自己的空值,但你需要为每个函数类型一个。例如,

void void_int_NULL(int n) { (void)n; abort(); }

然后你可以测试

if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);

丑陋,尼特?

于 2008-12-06T01:20:43.227 回答